diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1bfa7a41ccc9..ffc0150ebac5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,53 +1,18 @@ -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- +FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm -FROM node:8 +RUN apt-get install -y wget bzip2 -# Avoid warnings by switching to noninteractive -ENV DEBIAN_FRONTEND=noninteractive +# Run in silent mode and save downloaded script as anaconda.sh. +# Run with /bin/bash and run in silent mode to /opt/conda. +# Also get rid of installation script after finishing. +RUN wget --quiet https://repo.anaconda.com/archive/Anaconda3-2023.07-1-Linux-x86_64.sh -O ~/anaconda.sh && \ + /bin/bash ~/anaconda.sh -b -p /opt/conda && \ + rm ~/anaconda.sh -# 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 +ENV PATH="/opt/conda/bin:$PATH" + +# Sudo apt update needs to run in order for installation of fish to work . +RUN sudo apt update && \ + sudo apt install fish -y -# 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 index 17c0189dcafd..67a8833d30cf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,26 +1,30 @@ -// 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 +// For format details, see https://aka.ms/devcontainer.json. { - "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" + "name": "VS Code Python Dev Container", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + "dockerfile": "./Dockerfile", + "context": ".." }, + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy" + ] + } + }, + // Commands to execute on container creation,start. + "postCreateCommand": "bash scripts/postCreateCommand.sh", + "onCreateCommand": "bash scripts/onCreateCommand.sh", - // 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" ], + "containerEnv": { + "CI_PYTHON_PATH": "/workspaces/vscode-python/.venv/bin/python" + } - // 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/.eslintignore b/.eslintignore deleted file mode 100644 index 8ae5de73e229..000000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -src/ipywidgets/* diff --git a/.eslintplugin/no-bad-gdpr-comment.js b/.eslintplugin/no-bad-gdpr-comment.js new file mode 100644 index 000000000000..786259683ff6 --- /dev/null +++ b/.eslintplugin/no-bad-gdpr-comment.js @@ -0,0 +1,51 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +var noBadGDPRComment = { + create: function (context) { + var _a; + return _a = {}, + _a['Program'] = function (node) { + for (var _i = 0, _a = node.comments; _i < _a.length; _i++) { + var comment = _a[_i]; + if (comment.type !== 'Block' || !comment.loc) { + continue; + } + if (!comment.value.includes('__GDPR__')) { + continue; + } + var dataStart = comment.value.indexOf('\n'); + var data = comment.value.substring(dataStart); + var gdprData = void 0; + try { + var jsonRaw = "{ ".concat(data, " }"); + gdprData = JSON.parse(jsonRaw); + } + catch (e) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: 'GDPR comment is not valid JSON', + }); + } + if (gdprData) { + var len = Object.keys(gdprData).length; + if (len !== 1) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: "GDPR comment must contain exactly one key, not ".concat(Object.keys(gdprData).join(', ')), + }); + } + } + } + }, + _a; + }, +}; +module.exports = { + rules: { + 'no-bad-gdpr-comment': noBadGDPRComment, // Ensure correct structure + }, +}; diff --git a/.eslintplugin/no-bad-gdpr-comment.ts b/.eslintplugin/no-bad-gdpr-comment.ts new file mode 100644 index 000000000000..1eba899a7de3 --- /dev/null +++ b/.eslintplugin/no-bad-gdpr-comment.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +const noBadGDPRComment: eslint.Rule.RuleModule = { + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + ['Program'](node) { + for (const comment of (node as eslint.AST.Program).comments) { + if (comment.type !== 'Block' || !comment.loc) { + continue; + } + if (!comment.value.includes('__GDPR__')) { + continue; + } + + const dataStart = comment.value.indexOf('\n'); + const data = comment.value.substring(dataStart); + + let gdprData: { [key: string]: object } | undefined; + + try { + const jsonRaw = `{ ${data} }`; + gdprData = JSON.parse(jsonRaw); + } catch (e) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: 'GDPR comment is not valid JSON', + }); + } + + if (gdprData) { + const len = Object.keys(gdprData).length; + if (len !== 1) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: `GDPR comment must contain exactly one key, not ${Object.keys(gdprData).join( + ', ', + )}`, + }); + } + } + } + }, + }; + }, +}; + +module.exports = { + rules: { + 'no-bad-gdpr-comment': noBadGDPRComment, // Ensure correct structure + }, +}; diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a74911001e09..000000000000 --- a/.eslintrc +++ /dev/null @@ -1,109 +0,0 @@ -{ - "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 - "comma-dangle": "off", - "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 - } - ], - "max-classes-per-file": "off", - "max-len": [ - "error", - { - "code": 120 - } - ], - "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/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..e2c2a50781b9 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# Prettier +2b6a8f2d439fe9d5e66665ea46d8b690ac9b2c39 +649156a09ccdc51c0d20f7cd44540f1918f9347b +4f774d94bf4fbf87bb417b2b2b8e79e334eb3536 +61b179b2092050709e3c373a6738abad8ce581c4 +c33617b0b98daeb4d72040b48c5850b476d6256c +db8e1e2460e9754ec0672d958789382b6d15c5aa +08bc9ad3bee5b19f02fa756fbc53ab32f1b39920 +# Black +a58eeffd1b64498e2afe5f11597888dfd1c8699c +5cd8f539f4d2086b718c8f11f823c0ac12fc2c49 +9ec9e9eaebb25adc6d942ac19d4d6c128abb987f +c4af91e090057d20d7a633b3afa45eaa13ece76f +# Ruff +e931bed3efbede7b05113316506958ecd7506777 diff --git a/.github/ISSUE_TEMPLATE/1_ds_bug_report.md b/.github/ISSUE_TEMPLATE/1_ds_bug_report.md deleted file mode 100644 index ab52c81e6a1e..000000000000 --- a/.github/ISSUE_TEMPLATE/1_ds_bug_report.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -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 | ... - -## Developer Tools Console 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 deleted file mode 100644 index 63ebddf67b47..000000000000 --- a/.github/ISSUE_TEMPLATE/2_bug_report.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -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 deleted file mode 100644 index 71876c9f3aad..000000000000 --- a/.github/ISSUE_TEMPLATE/3_ds_feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -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/3_feature_request.md b/.github/ISSUE_TEMPLATE/3_feature_request.md new file mode 100644 index 000000000000..d13a5e94e700 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_feature_request.md @@ -0,0 +1,7 @@ +--- +name: Feature request +about: Request for the Python extension, not supporting/sibling extensions +labels: classify, feature-request +--- + + diff --git a/.github/ISSUE_TEMPLATE/4_feature_request.md b/.github/ISSUE_TEMPLATE/4_feature_request.md deleted file mode 100644 index e98872ab0d10..000000000000 --- a/.github/ISSUE_TEMPLATE/4_feature_request.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -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 index ef0e2aae348f..c966f6bde856 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,17 @@ blank_issues_enabled: false contact_links: - - name: Stack Overflow - url: https://stackoverflow.com/questions/tagged/visual-studio-code+python - about: Please ask questions here. + - name: 'Bug 🐜' + url: https://aka.ms/pvsc-bug + about: 'Use the `Python: Report Issue...` command (follow the link for instructions)' + - name: 'Pylance' + url: https://github.com/microsoft/pylance-release/issues + about: 'For issues relating to the Pylance language server extension' + - name: 'Jupyter' + url: https://github.com/microsoft/vscode-jupyter/issues + about: 'For issues relating to the Jupyter extension (including the interactive window)' + - name: 'Python Debugger' + url: https://github.com/microsoft/vscode-python-debugger/issues + about: 'For issues relating to the Python debugger' + - name: Help/Support + url: https://github.com/microsoft/vscode-python/discussions/categories/q-a + about: 'Having trouble with the extension? Need help getting something to work?' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 1d41e4544cbd..000000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,17 +0,0 @@ -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/master/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/master/.github/test_plan.md) is updated as appropriate. -- [ ] [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/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..912ff2c34a74 --- /dev/null +++ b/.github/actions/build-vsix/action.yml @@ -0,0 +1,101 @@ +name: 'Build VSIX' +description: "Build the extension's VSIX" + +inputs: + node_version: + description: 'Version of Node to install' + required: true + vsix_name: + description: 'Name to give the final VSIX' + required: true + artifact_name: + description: 'Name to give the artifact containing the VSIX' + required: true + cargo_target: + description: 'Cargo build target for the native build' + required: true + vsix_target: + description: 'vsix build target for the native build' + required: true + +runs: + using: 'composite' + steps: + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. + - name: Use Python 3.10 for JediLSP + uses: actions/setup-python@v6 + with: + python-version: '3.10' + cache: 'pip' + cache-dependency-path: | + requirements.txt + python_files/jedilsp_requirements/requirements.txt + + - name: Upgrade Pip + run: python -m pip install -U pip + shell: bash + + # For faster/better builds of sdists. + - name: Install build pre-requisite + run: python -m pip install wheel nox + shell: bash + + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs + shell: bash + + - name: Add Rustup target + run: rustup target add "${CARGO_TARGET}" + shell: bash + env: + CARGO_TARGET: ${{ inputs.cargo_target }} + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + env: + CARGO_TARGET: ${{ inputs.cargo_target }} + + - name: Run npm ci + run: npm ci --prefer-offline + shell: bash + + - name: Update optional extension dependencies + run: npm run addExtensionPackDependencies + shell: bash + + - name: Build Webpack + run: | + npx gulp clean + npx gulp prePublishBundle + shell: bash + + - name: Build VSIX + run: npx vsce package --target "${VSIX_TARGET}" --out ms-python-insiders.vsix --pre-release + shell: bash + env: + VSIX_TARGET: ${{ inputs.vsix_target }} + + - name: Rename VSIX + # Move to a temp name in case the specified name happens to match the default name. + run: mv ms-python-insiders.vsix ms-python-temp.vsix && mv ms-python-temp.vsix "${VSIX_NAME}" + shell: bash + env: + VSIX_NAME: ${{ inputs.vsix_name }} + + - name: Upload VSIX + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.artifact_name }} + path: ${{ inputs.vsix_name }} + if-no-files-found: error + retention-days: 7 diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml new file mode 100644 index 000000000000..0bd5a2d8e1e2 --- /dev/null +++ b/.github/actions/lint/action.yml @@ -0,0 +1,50 @@ +name: 'Lint' +description: 'Lint TypeScript and Python code' + +inputs: + node_version: + description: 'Version of Node to install' + required: true + +runs: + using: 'composite' + steps: + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + + - name: Install Node dependencies + run: npm ci --prefer-offline + shell: bash + + - name: Run `gulp prePublishNonBundle` + run: npx gulp prePublishNonBundle + shell: bash + + - name: Check dependencies + run: npm run checkDependencies + shell: bash + + - name: Lint TypeScript code + run: npm run lint + shell: bash + + - name: Check TypeScript format + run: npm run format-check + shell: bash + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + cache: 'pip' + + - name: Run Ruff + run: | + python -m pip install -U "ruff" + python -m ruff check . + python -m ruff format --check + working-directory: python_files + shell: bash diff --git a/.github/actions/smoke-tests/action.yml b/.github/actions/smoke-tests/action.yml new file mode 100644 index 000000000000..0531ef5d42a3 --- /dev/null +++ b/.github/actions/smoke-tests/action.yml @@ -0,0 +1,66 @@ +name: 'Smoke tests' +description: 'Run smoke tests' + +inputs: + node_version: + description: 'Version of Node to install' + required: true + artifact_name: + description: 'Name of the artifact containing the VSIX' + required: true + +runs: + using: 'composite' + steps: + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + cache: 'pip' + cache-dependency-path: | + build/test-requirements.txt + requirements.txt + + - name: Install dependencies (npm ci) + run: npm ci --prefer-offline + shell: bash + + - name: Install Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + options: '-t ./python_files/lib/python --implementation py' + + - name: pip install system test requirements + run: | + python -m pip install --upgrade -r build/test-requirements.txt + shell: bash + + # Bits from the VSIX are reused by smokeTest.ts to speed things up. + - name: Download VSIX + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact_name }} + + - name: Prepare for smoke tests + run: npx tsc -p ./ + shell: bash + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "CI_PYTHON_PATH=python" >> $GITHUB_ENV + echo "CI_DISABLE_AUTO_SELECTION=1" >> $GITHUB_ENV + shell: bash + + - name: Run smoke tests + env: + DISPLAY: 10 + INSTALL_JUPYTER_EXTENSION: true + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: node --no-force-async-hooks-checks ./out/test/smokeTest.js diff --git a/.github/commands.json b/.github/commands.json new file mode 100644 index 000000000000..2fb6684a7ee6 --- /dev/null +++ b/.github/commands.json @@ -0,0 +1,157 @@ +[ + { + "type": "label", + "name": "*question", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because it is a question about using the Python extension for VS Code rather than an issue or feature request. We recommend browsing resources such as our [Python documentation](https://code.visualstudio.com/docs/languages/python) and our [Discussions page](https://github.com/microsoft/vscode-python/discussions). You may also find help on [StackOverflow](https://stackoverflow.com/questions/tagged/vscode-python), where the community has already answered thousands of similar questions. \n\nHappy Coding!" + }, + { + "type": "label", + "name": "*dev-question", + "action": "close", + "reason": "not_planned", + "comment": "We have a great extension developer community over on [GitHub discussions](https://github.com/microsoft/vscode-discussions/discussions) and [Slack](https://vscode-dev-community.slack.com/) where extension authors help each other. This is a great place for you to ask questions and find support.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*extension-candidate", + "action": "close", + "reason": "not_planned", + "comment": "We try to keep the Python extension lean and we think the functionality you're asking for is great for a VS Code extension. You might be able to find one that suits you in the [VS Code Marketplace](https://aka.ms/vscodemarketplace) already. If not, in a few simple steps you can get started [writing your own extension](https://aka.ms/vscodewritingextensions) or leverage our [tool extension template](https://github.com/microsoft/vscode-python-tools-extension-template) to get started. In addition, check out the [vscode-python-environments](https://github.com/microsoft/vscode-python-environments) as this may be the right spot for your request. \n\nHappy Coding!" + }, + { + "type": "label", + "name": "*not-reproducible", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we are unable to reproduce the problem with the steps you describe. Chances are we've already fixed your problem in a recent version of the Python extension, so we recommend updating to the latest version and trying again. If you continue to experience this issue, please ask us to reopen the issue and provide us with more detail.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*out-of-scope", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we [don't plan to address it](https://github.com/microsoft/vscode-python/wiki/Issue-Management#criteria-for-closing-out-of-scope-feature-requests) in the foreseeable future. If you disagree and feel that this issue is crucial: we are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/pythonvscoderoadmap) and [issue reporting guidelines]( https://github.com/microsoft/vscode-python/wiki/Issue-Management).\n\nThanks for your understanding, and happy coding!" + }, + { + "type": "label", + "name": "wont-fix", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we [don't plan to address it](https://github.com/microsoft/vscode/wiki/Issue-Grooming#wont-fix-bugs).\n\nThanks for your understanding, and happy coding!" + }, + { + "type": "label", + "name": "*caused-by-extension", + "action": "close", + "reason": "not_planned", + "comment": "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). If you don't know which extension is causing the problem, you can run `Help: Start extension bisect` from the command palette (F1) to help identify the problem extension.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*as-designed", + "action": "close", + "reason": "not_planned", + "comment": "The described behavior is how it is expected to work. If you disagree, please explain what is expected and what is not in more detail. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "label", + "name": "L10N", + "assign": [ + "csigs", + "TylerLeonhardt" + ] + }, + { + "type": "label", + "name": "*duplicate", + "action": "close", + "reason": "not_planned", + "comment": "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for [similar existing issues](${duplicateQuery}). See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "comment", + "name": "verified", + "allowUsers": [ + "@author" + ], + "action": "updateLabels", + "addLabel": "verified", + "removeLabel": "author-verification-requested", + "requireLabel": "author-verification-requested", + "disallowLabel": "unreleased" + }, + { + "type": "comment", + "name": "confirm", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "confirmed", + "removeLabel": "confirmation-pending" + }, + { + "type": "label", + "name": "*off-topic", + "action": "close", + "reason": "not_planned", + "comment": "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "comment", + "name": "gifPlease", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "comment", + "addLabel": "info-needed", + "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette). Lastly, please attach this file via the GitHub web interface as emailed responses will strip files out from the issue.\n\nHappy coding!" + }, + { + "type": "label", + "name": "*workspace-trust-docs", + "action": "close", + "reason": "not_planned", + "comment": "This issue appears to be the result of the new workspace trust feature shipped in June 2021. This security-focused feature has major impact on the functionality of VS Code. Due to the volume of issues, we ask that you take some time to review our [comprehensive documentation](https://aka.ms/vscode-workspace-trust) on the feature. If your issue is still not resolved, please let us know." + }, + { + "type": "label", + "name": "~verification-steps-needed", + "action": "updateLabels", + "addLabel": "verification-steps-needed", + "removeLabel": "~verification-steps-needed", + "comment": "Friendly ping! Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." + }, + { + "type": "label", + "name": "~info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/pvsc-bug). Please take the time to review these and update the issue or even open a new one with the Report Issue command in VS Code (**Help > Report Issue**) to have all the right information collected for you.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~version-info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~version-info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our issue reporting guidelines. Please take the time to review these and update the issue or even open a new one with the Report Issue command in VS Code (**Help > Report Issue**) to have all the right information collected for you.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~confirmation-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~confirmation-needed", + "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" + } +] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..14c8e18d475d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,49 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: / + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/build-vsix + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/lint + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/smoke-test + schedule: + interval: daily + labels: + - 'no-changelog' + + # Not skipping the news for some Python dependencies in case it's actually useful to communicate to users. + - package-ecosystem: 'pip' + directory: / + schedule: + interval: daily + ignore: + - dependency-name: prospector # Due to Python 2.7 and #14477. + - dependency-name: pytest # Due to Python 2.7 and #13776. + - dependency-name: py # Due to Python 2.7. + - dependency-name: jedi-language-server + labels: + - 'no-changelog' + # Activate when we feel ready to keep up with frequency. + # - package-ecosystem: 'npm' + # directory: / + # schedule: + # interval: daily + # default_labels: + # - "no-changelog" diff --git a/.github/instructions/learning.instructions.md b/.github/instructions/learning.instructions.md new file mode 100644 index 000000000000..28b085f486ce --- /dev/null +++ b/.github/instructions/learning.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: '**' +description: This document describes how to deal with learnings that you make. (meta instruction) +--- + +This document describes how to deal with learnings that you make. +It is a meta-instruction file. + +Structure of learnings: + +- Each instruction file has a "Learnings" section. +- Each learning has a counter that indicates how often that learning was useful (initially 1). +- Each learning has a 1 sentence description of the learning that is clear and concise. + +Example: + +```markdown +## Learnings + +- Prefer `const` over `let` whenever possible (1) +- Avoid `any` type (3) +``` + +When the user tells you "learn!", you should: + +- extract a learning from the recent conversation + _ identify the problem that you created + _ identify why it was a problem + _ identify how you were told to fix it/how the user fixed it + _ generate only one learning (1 sentence) that helps to summarize the insight gained +- then, add the reflected learning to the "Learnings" section of the most appropriate instruction file + +Important: Whenever a learning was really useful, increase the counter!! +When a learning was not useful and just caused more problems, decrease the counter. diff --git a/.github/instructions/pytest-json-test-builder.instructions.md b/.github/instructions/pytest-json-test-builder.instructions.md new file mode 100644 index 000000000000..436bce0c9cd8 --- /dev/null +++ b/.github/instructions/pytest-json-test-builder.instructions.md @@ -0,0 +1,126 @@ +--- +applyTo: 'python_files/tests/pytestadapter/test_discovery.py' +description: 'A guide for adding new tests for pytest discovery and JSON formatting in the test_pytest_collect suite.' +--- + +# How to Add New Pytest Discovery Tests + +This guide explains how to add new tests for pytest discovery and JSON formatting in the `test_pytest_collect` suite. Follow these steps to ensure your tests are consistent and correct. + +--- + +## 1. Add Your Test File + +- Place your new test file/files in the appropriate subfolder under: + ``` + python_files/tests/pytestadapter/.data/ + ``` +- Organize folders and files to match the structure you want to test. For example, to test nested folders, create the corresponding directory structure. +- In your test file, mark each test function with a comment: + ```python + def test_function(): # test_marker--test_function + ... + ``` + +**Root Node Matching:** + +- The root node in your expected output must match the folder or file you pass to pytest discovery. For example, if you run discovery on a subfolder, the root `"name"`, `"path"`, and `"id_"` in your expected output should be that subfolder, not the parent `.data` folder. +- Only use `.data` as the root if you are running discovery on the entire `.data` folder. + +**Example:** +If you run: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +{ + "name": "myfolder", + "path": os.fspath(TEST_DATA_PATH / "myfolder"), + "type_": "folder", + ... +} +``` + +--- + +## 2. Update `expected_discovery_test_output.py` + +- Open `expected_discovery_test_output.py` in the same test suite. +- Add a new expected output dictionary for your test file, following the format of existing entries. +- Use the helper functions and path conventions: + - Use `os.fspath()` for all paths. + - Use `find_test_line_number("function_name", file_path)` for the `lineno` field. + - Use `get_absolute_test_id("relative_path::function_name", file_path)` for `id_` and `runID`. + - Always use current path concatenation (e.g., `TEST_DATA_PATH / "your_folder" / "your_file.py"`). + - Create new constants as needed to keep the code clean and maintainable. + +**Important:** + +- Do **not** read the entire `expected_discovery_test_output.py` file if you only need to add or reference a single constant. This file is very large; prefer searching for the relevant section or appending to the end. + +**Example:** +If you run discovery on a subfolder: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +myfolder_path = TEST_DATA_PATH / "myfolder" +my_expected_output = { + "name": "myfolder", + "path": os.fspath(myfolder_path), + "type_": "folder", + ... +} +``` + +- Add a comment above your dictionary describing the structure, as in the existing examples. + +--- + +## 3. Add Your Test to `test_discovery.py` + +- In `test_discovery.py`, add your new test as a parameterized case to the main `test_pytest_collect` function. Do **not** create a standalone test function for new discovery cases. +- Reference your new expected output constant from `expected_discovery_test_output.py`. + +**Example:** + +```python +@pytest.mark.parametrize( + ("file", "expected_const"), + [ + ("myfolder", my_expected_output), + # ... other cases ... + ], +) +def test_pytest_collect(file, expected_const): + ... +``` + +--- + +## 4. Run and Verify + +- Run the test suite to ensure your new test is discovered and passes. +- If the test fails, check your expected output dictionary for path or structure mismatches. + +--- + +## 5. Tips + +- Always use the helper functions for line numbers and IDs. +- Match the folder/file structure in `.data` to the expected JSON structure. +- Use comments to document the expected output structure for clarity. +- Ensure all `"path"` and `"id_"` fields in your expected output match exactly what pytest returns, including absolute paths and root node structure. + +--- + +**Reference:** +See `expected_discovery_test_output.py` for more examples and formatting. Use search or jump to the end of the file to avoid reading the entire file when possible. diff --git a/.github/instructions/python-quality-checks.instructions.md b/.github/instructions/python-quality-checks.instructions.md new file mode 100644 index 000000000000..48f37529dfbc --- /dev/null +++ b/.github/instructions/python-quality-checks.instructions.md @@ -0,0 +1,97 @@ +--- +applyTo: 'python_files/**' +description: Guide for running and fixing Python quality checks (Ruff and Pyright) that run in CI +--- + +# Python Quality Checks — Ruff and Pyright + +Run the same Python quality checks that run in CI. All checks target `python_files/` and use config from `python_files/pyproject.toml`. + +## Commands + +```bash +npm run check-python # Run both Ruff and Pyright +npm run check-python:ruff # Linting and formatting only +npm run check-python:pyright # Type checking only +``` + +## Fixing Ruff Errors + +**Auto-fix most issues:** + +```bash +cd python_files +python -m ruff check . --fix +python -m ruff format +npm run check-python:ruff # Verify +``` + +**Manual fixes:** + +- Ruff shows file, line number, rule code (e.g., `F841`), and description +- Open the file, read the error, fix the code +- Common: line length (100 char max), import sorting, unused variables + +## Fixing Pyright Errors + +**Common patterns and fixes:** + +- **Undefined variable/import**: Add the missing import +- **Type mismatch**: Correct the type or add type annotations +- **Missing return type**: Add `-> ReturnType` to function signatures + ```python + def my_function() -> str: # Add return type + return "result" + ``` + +**Verify:** + +```bash +npm run check-python:pyright +``` + +## Configuration + +- **Ruff**: Line length 100, Python 3.9+, 40+ rule families (flake8, isort, pyupgrade, etc.) +- **Pyright**: Version 1.1.308 (or whatever is found in the environment), ignores `lib/` and 15+ legacy files +- Config: `python_files/pyproject.toml` sections `[tool.ruff]` and `[tool.pyright]` + +## Troubleshooting + +**"Module not found" in Pyright**: Install dependencies + +```bash +python -m pip install --upgrade -r build/test-requirements.txt +nox --session install_python_libs +``` + +**Import order errors**: Auto-fix with `ruff check . --fix` + +**Type errors in ignored files**: Legacy files in `pyproject.toml` ignore list—fix if working on them + +## When Writing Tests + +**Always format your test files before committing:** + +```bash +cd python_files +ruff format tests/ # Format all test files +# or format specific files: +ruff format tests/unittestadapter/test_utils.py +``` + +**Best practice workflow:** + +1. Write your test code +2. Run `ruff format` on the test files +3. Run the tests to verify they pass +4. Run `npm run check-python` to catch any remaining issues + +This ensures your tests pass both functional checks and quality checks in CI. + +## Learnings + +- Always run `npm run check-python` before pushing to catch CI failures early (1) +- Use `ruff check . --fix` to auto-fix most linting issues before manual review (1) +- Pyright version must match CI (1.1.308) to avoid inconsistent results between local and CI runs (1) +- Always run `ruff format` on test files after writing them to avoid formatting CI failures (1) diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md new file mode 100644 index 000000000000..844946404328 --- /dev/null +++ b/.github/instructions/testing-workflow.instructions.md @@ -0,0 +1,581 @@ +--- +applyTo: '**/test/**' +--- + +# AI Testing Workflow Guide: Write, Run, and Fix Tests + +This guide provides comprehensive instructions for AI agents on the complete testing workflow: writing tests, running them, diagnosing failures, and fixing issues. Use this guide whenever working with test files or when users request testing tasks. + +## Complete Testing Workflow + +This guide covers the full testing lifecycle: + +1. **📝 Writing Tests** - Create comprehensive test suites +2. **▶️ Running Tests** - Execute tests using VS Code tools +3. **🔍 Diagnosing Issues** - Analyze failures and errors +4. **🛠️ Fixing Problems** - Resolve compilation and runtime issues +5. **✅ Validation** - Ensure coverage and resilience + +### When to Use This Guide + +**User Requests Testing:** + +- "Write tests for this function" +- "Run the tests" +- "Fix the failing tests" +- "Test this code" +- "Add test coverage" + +**File Context Triggers:** + +- Working in `**/test/**` directories +- Files ending in `.test.ts` or `.unit.test.ts` +- Test failures or compilation errors +- Coverage reports or test output analysis + +## Test Types + +When implementing tests as an AI agent, choose between two main types: + +### Unit Tests (`*.unit.test.ts`) + +- **Fast isolated testing** - Mock all external dependencies +- **Use for**: Pure functions, business logic, data transformations +- **Execute with**: `runTests` tool with specific file patterns +- **Mock everything** - VS Code APIs automatically mocked via `/src/test/unittests.ts` + +### Extension Tests (`*.test.ts`) + +- **Full VS Code integration** - Real environment with actual APIs +- **Use for**: Command registration, UI interactions, extension lifecycle +- **Execute with**: VS Code launch configurations or `runTests` tool +- **Slower but comprehensive** - Tests complete user workflows + +## 🤖 Agent Tool Usage for Test Execution + +### Primary Tool: `runTests` + +Use the `runTests` tool to execute tests programmatically rather than terminal commands for better integration and result parsing: + +```typescript +// Run specific test files +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'run', +}); + +// Run tests with coverage +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'coverage', + coverageFiles: ['/absolute/path/to/source.ts'], +}); + +// Run specific test names +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + testNames: ['should handle edge case', 'should validate input'], +}); +``` + +### Compilation Requirements + +Before running tests, ensure compilation. Always start compilation with `npm run watch-tests` before test execution to ensure TypeScript files are built. Recompile after making import/export changes before running tests, as stubs won't work if they're applied to old compiled JavaScript that doesn't have the updated imports: + +```typescript +// Start watch mode for auto-compilation +await run_in_terminal({ + command: 'npm run watch-tests', + isBackground: true, + explanation: 'Start test compilation in watch mode', +}); + +// Or compile manually +await run_in_terminal({ + command: 'npm run compile-tests', + isBackground: false, + explanation: 'Compile TypeScript test files', +}); +``` + +### Alternative: Terminal Execution + +For targeted test runs when `runTests` tool is unavailable. Note: When a targeted test run yields 0 tests, first verify the compiled JS exists under `out/test` (rootDir is `src`); absence almost always means the test file sits outside `src` or compilation hasn't run yet: + +```typescript +// Run specific test suite +await run_in_terminal({ + command: 'npm run unittest -- --grep "Suite Name"', + isBackground: false, + explanation: 'Run targeted unit tests', +}); +``` + +## 🔍 Diagnosing Test Failures + +### Common Failure Patterns + +**Compilation Errors:** + +```typescript +// Missing imports +if (error.includes('Cannot find module')) { + await addMissingImports(testFile); +} + +// Type mismatches +if (error.includes("Type '" && error.includes("' is not assignable"))) { + await fixTypeIssues(testFile); +} +``` + +**Runtime Errors:** + +```typescript +// Mock setup issues +if (error.includes('stub') || error.includes('mock')) { + await fixMockConfiguration(testFile); +} + +// Assertion failures +if (error.includes('AssertionError')) { + await analyzeAssertionFailure(error); +} +``` + +### Systematic Failure Analysis + +Fix test issues iteratively - run tests, analyze failures, apply fixes, repeat until passing. When unit tests fail with VS Code API errors like `TypeError: X is not a constructor` or `Cannot read properties of undefined (reading 'Y')`, check if VS Code APIs are properly mocked in `/src/test/unittests.ts` - add missing APIs following the existing pattern. + +```typescript +interface TestFailureAnalysis { + type: 'compilation' | 'runtime' | 'assertion' | 'timeout'; + message: string; + location: { file: string; line: number; col: number }; + suggestedFix: string; +} + +function analyzeFailure(failure: TestFailure): TestFailureAnalysis { + if (failure.message.includes('Cannot find module')) { + return { + type: 'compilation', + message: failure.message, + location: failure.location, + suggestedFix: 'Add missing import statement', + }; + } + // ... other failure patterns +} +``` + +### Agent Decision Logic for Test Type Selection + +**Choose Unit Tests (`*.unit.test.ts`) when analyzing:** + +- Functions with clear inputs/outputs and no VS Code API dependencies +- Data transformation, parsing, or utility functions +- Business logic that can be isolated with mocks +- Error handling scenarios with predictable inputs + +**Choose Extension Tests (`*.test.ts`) when analyzing:** + +- Functions that register VS Code commands or use `vscode.*` APIs +- UI components, tree views, or command palette interactions +- File system operations requiring workspace context +- Extension lifecycle events (activation, deactivation) + +**Agent Implementation Pattern:** + +```typescript +function determineTestType(functionCode: string): 'unit' | 'extension' { + if ( + functionCode.includes('vscode.') || + functionCode.includes('commands.register') || + functionCode.includes('window.') || + functionCode.includes('workspace.') + ) { + return 'extension'; + } + return 'unit'; +} +``` + +## 🎯 Step 1: Automated Function Analysis + +As an AI agent, analyze the target function systematically: + +### Code Analysis Checklist + +```typescript +interface FunctionAnalysis { + name: string; + inputs: string[]; // Parameter types and names + outputs: string; // Return type + dependencies: string[]; // External modules/APIs used + sideEffects: string[]; // Logging, file system, network calls + errorPaths: string[]; // Exception scenarios + testType: 'unit' | 'extension'; +} +``` + +### Analysis Implementation + +1. **Read function source** using `read_file` tool +2. **Identify imports** - look for `vscode.*`, `child_process`, `fs`, etc. +3. **Map data flow** - trace inputs through transformations to outputs +4. **Catalog dependencies** - external calls that need mocking +5. **Document side effects** - logging, file operations, state changes + +### Test Setup Differences + +#### Unit Test Setup (\*.unit.test.ts) + +```typescript +// Mock VS Code APIs - handled automatically by unittests.ts +import * as sinon from 'sinon'; +import * as workspaceApis from '../../common/workspace.apis'; // Wrapper functions + +// Stub wrapper functions, not VS Code APIs directly +// Always mock wrapper functions (e.g., workspaceApis.getConfiguration()) instead of +// VS Code APIs directly to avoid stubbing issues +const mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); +``` + +#### Extension Test Setup (\*.test.ts) + +```typescript +// Use real VS Code APIs +import * as vscode from 'vscode'; + +// Real VS Code APIs available - no mocking needed +const config = vscode.workspace.getConfiguration('python'); +``` + +## 🎯 Step 2: Generate Test Coverage Matrix + +Based on function analysis, automatically generate comprehensive test scenarios: + +### Coverage Matrix Generation + +```typescript +interface TestScenario { + category: 'happy-path' | 'edge-case' | 'error-handling' | 'side-effects'; + description: string; + inputs: Record; + expectedOutput?: any; + expectedSideEffects?: string[]; + shouldThrow?: boolean; +} +``` + +### Automated Scenario Creation + +1. **Happy Path**: Normal execution with typical inputs +2. **Edge Cases**: Boundary conditions, empty/null inputs, unusual but valid data +3. **Error Scenarios**: Invalid inputs, dependency failures, exception paths +4. **Side Effects**: Verify logging calls, file operations, state changes + +### Agent Pattern for Scenario Generation + +```typescript +function generateTestScenarios(analysis: FunctionAnalysis): TestScenario[] { + const scenarios: TestScenario[] = []; + + // Generate happy path for each input combination + scenarios.push(...generateHappyPathScenarios(analysis)); + + // Generate edge cases for boundary conditions + scenarios.push(...generateEdgeCaseScenarios(analysis)); + + // Generate error scenarios for each dependency + scenarios.push(...generateErrorScenarios(analysis)); + + return scenarios; +} +``` + +## 🗺️ Step 3: Plan Your Test Coverage + +### Create a Test Coverage Matrix + +#### Main Flows + +- ✅ **Happy path scenarios** - normal expected usage +- ✅ **Alternative paths** - different configuration combinations +- ✅ **Integration scenarios** - multiple features working together + +#### Edge Cases + +- 🔸 **Boundary conditions** - empty inputs, missing data +- 🔸 **Error scenarios** - network failures, permission errors +- 🔸 **Data validation** - invalid inputs, type mismatches + +#### Real-World Scenarios + +- ✅ **Fresh install** - clean slate +- ✅ **Existing user** - migration scenarios +- ✅ **Power user** - complex configurations +- 🔸 **Error recovery** - graceful degradation + +### Example Test Plan Structure + +```markdown +## Test Categories + +### 1. Configuration Migration Tests + +- No legacy settings exist +- Legacy settings already migrated +- Fresh migration needed +- Partial migration required +- Migration failures + +### 2. Configuration Source Tests + +- Global search paths +- Workspace search paths +- Settings precedence +- Configuration errors + +### 3. Path Resolution Tests + +- Absolute vs relative paths +- Workspace folder resolution +- Path validation and filtering + +### 4. Integration Scenarios + +- Combined configurations +- Deduplication logic +- Error handling flows +``` + +## 🔧 Step 4: Set Up Your Test Infrastructure + +### Test File Structure + +```typescript +// 1. Imports - group logically +import assert from 'node:assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as logging from '../../../common/logging'; +import * as pathUtils from '../../../common/utils/pathUtils'; +import * as workspaceApis from '../../../common/workspace.apis'; + +// 2. Function under test +import { getAllExtraSearchPaths } from '../../../managers/common/nativePythonFinder'; + +// 3. Mock interfaces +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} +``` + +### Mock Setup Strategy + +Create minimal mock objects with only required methods and use TypeScript type assertions (e.g., `mockApi as PythonEnvironmentApi`) to satisfy interface requirements instead of implementing all interface methods when only specific methods are needed for the test. Simplify mock setup by only mocking methods actually used in tests and use `as unknown as Type` for TypeScript compatibility. + +```typescript +suite('Function Integration Tests', () => { + // 1. Declare all mocks + let mockGetConfiguration: sinon.SinonStub; + let mockGetWorkspaceFolders: sinon.SinonStub; + let mockTraceLog: sinon.SinonStub; + let mockTraceError: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + + // 2. Mock complex objects + let pythonConfig: MockWorkspaceConfig; + let envConfig: MockWorkspaceConfig; + + setup(() => { + // 3. Initialize all mocks + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + mockGetWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + mockTraceLog = sinon.stub(logging, 'traceLog'); + mockTraceError = sinon.stub(logging, 'traceError'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + + // 4. Set up default behaviors + mockGetWorkspaceFolders.returns(undefined); + + // 5. Create mock configuration objects + // When fixing mock environment creation, use null to truly omit + // properties rather than undefined + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + envConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + }); + + teardown(() => { + sinon.restore(); // Always clean up! + }); +}); +``` + +## Step 4: Write Tests Using Mock → Run → Assert Pattern + +### The Three-Phase Pattern + +#### Phase 1: Mock (Set up the scenario) + +```typescript +test('Description of what this tests', async () => { + // Mock → Clear description of the scenario + pythonConfig.inspect.withArgs('venvPath').returns({ globalValue: '/path' }); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + mockGetWorkspaceFolders.returns([{ uri: Uri.file('/workspace') }]); +``` + +#### Phase 2: Run (Execute the function) + +```typescript +// Run +const result = await getAllExtraSearchPaths(); +``` + +#### Phase 3: Assert (Verify the behavior) + +```typescript + // Assert - Use set-based comparison for order-agnostic testing + const expected = new Set(['/expected', '/paths']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + + // Verify side effects + // Use sinon.match() patterns for resilient assertions that don't break on minor output changes + assert(mockTraceLog.calledWith(sinon.match(/completion/i)), 'Should log completion'); +}); +``` + +## Step 6: Make Tests Resilient + +### Use Order-Agnostic Comparisons + +```typescript +// ❌ Brittle - depends on order +assert.deepStrictEqual(result, ['/path1', '/path2', '/path3']); + +// ✅ Resilient - order doesn't matter +const expected = new Set(['/path1', '/path2', '/path3']); +const actual = new Set(result); +assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); +assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); +``` + +### Use Flexible Error Message Testing + +```typescript +// ❌ Brittle - exact text matching +assert(mockTraceError.calledWith('Error during legacy python settings migration:')); + +// ✅ Resilient - pattern matching +assert(mockTraceError.calledWith(sinon.match.string, sinon.match.instanceOf(Error)), 'Should log migration error'); + +// ✅ Resilient - key terms with regex +assert(mockTraceError.calledWith(sinon.match(/migration.*error/i)), 'Should log migration error'); +``` + +### Handle Complex Mock Scenarios + +```typescript +// For functions that call the same mock multiple times +envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); +envConfig.inspect + .withArgs('globalSearchPaths') + .onSecondCall() + .returns({ + globalValue: ['/migrated/paths'], + }); + +// Testing async functions with child processes: +// Call the function first to get a promise, then use setTimeout to emit mock events, +// then await the promise - this ensures proper timing of mock setup versus function execution + +// Cannot stub internal function calls within the same module after import - stub external +// dependencies instead (e.g., stub childProcessApis.spawnProcess rather than trying to stub +// helpers.isUvInstalled when testing helpers.shouldUseUv) because intra-module calls use +// direct references, not module exports +``` + +## 🧪 Step 7: Test Categories and Patterns + +### Configuration Tests + +- Test different setting combinations +- Test setting precedence (workspace > user > default) +- Test configuration errors and recovery +- Always use dynamic path construction with Node.js `path` module when testing functions that resolve paths against workspace folders to ensure cross-platform compatibility + +### Data Flow Tests + +- Test how data moves through the system +- Test transformations (path resolution, filtering) +- Test state changes (migrations, updates) + +### Error Handling Tests + +- Test graceful degradation +- Test error logging +- Test fallback behaviors + +### Integration Tests + +- Test multiple features together +- Test real-world scenarios +- Test edge case combinations + +## 📊 Step 8: Review and Refine + +### Test Quality Checklist + +- [ ] **Clear naming** - test names describe the scenario and expected outcome +- [ ] **Good coverage** - main flows, edge cases, error scenarios +- [ ] **Resilient assertions** - won't break due to minor changes +- [ ] **Readable structure** - follows Mock → Run → Assert pattern +- [ ] **Isolated tests** - each test is independent +- [ ] **Fast execution** - tests run quickly with proper mocking + +### Common Anti-Patterns to Avoid + +- ❌ Testing implementation details instead of behavior +- ❌ Brittle assertions that break on cosmetic changes +- ❌ Order-dependent tests that fail due to processing changes +- ❌ Tests that don't clean up mocks properly +- ❌ Overly complex test setup that's hard to understand + +## 🔄 Reviewing and Improving Existing Tests + +### Quick Review Process + +1. **Read test files** - Check structure and mock setup +2. **Run tests** - Establish baseline functionality +3. **Apply improvements** - Use patterns below. When reviewing existing tests, focus on behavior rather than implementation details in test names and assertions +4. **Verify** - Ensure tests still pass + +### Common Fixes + +- Over-complex mocks → Minimal mocks with only needed methods +- Brittle assertions → Behavior-focused with error messages +- Vague test names → Clear scenario descriptions (transform "should return X when Y" into "should [expected behavior] when [scenario context]") +- Missing structure → Mock → Run → Assert pattern +- Untestable Node.js APIs → Create proxy abstraction functions (use function overloads to preserve intelligent typing while making functions mockable) + +## 🧠 Agent Learnings + +- When mocking `testController.createTestItem()` in unit tests, use `typemoq.It.isAny()` for parameters when testing handler behavior (not ID/label generation logic), but consider using specific matchers (e.g., `It.is((id: string) => id.startsWith('_error_'))`) when the actual values being passed are important for correctness - this balances test precision with maintainability (2) +- Remove unused variables from test code immediately - leftover tracking variables like `validationCallCount` that aren't referenced indicate dead code that should be simplified (1) +- Use `Uri.file(path).fsPath` for both sides of path comparisons in tests to ensure cross-platform compatibility - Windows converts forward slashes to backslashes automatically (1) +- When tests fail with "Cannot stub non-existent property", the method likely moved to a different class during refactoring - find the class that owns the method and test that class directly instead of stubbing on the original class (1) diff --git a/.github/instructions/testing_feature_area.instructions.md b/.github/instructions/testing_feature_area.instructions.md new file mode 100644 index 000000000000..a4e11523d7c8 --- /dev/null +++ b/.github/instructions/testing_feature_area.instructions.md @@ -0,0 +1,263 @@ +--- +applyTo: 'src/client/testing/**' +--- + +# Testing feature area — Discovery, Run, Debug, and Results + +This document maps the testing support in the extension: discovery, execution (run), debugging, result reporting and how those pieces connect to the codebase. It's written for contributors and agents who need to navigate, modify, or extend test support (both `unittest` and `pytest`). + +## Overview + +- Purpose: expose Python tests in the VS Code Test Explorer (TestController), support discovery, run, debug, and surface rich results and outputs. +- Scope: provider-agnostic orchestration + provider-specific adapters, TestController mapping, IPC with Python-side scripts, debug launch integration, and configuration management. + +## High-level architecture + +- Controller / UI bridge: orchestrates TestController requests and routes them to workspace adapters. +- Workspace adapter: provider-agnostic coordinator that translates TestController requests to provider adapters and maps payloads back into TestItems/TestRuns. +- Provider adapters: implement discovery/run/debug for `unittest` and `pytest` by launching Python scripts and wiring named-pipe IPC. +- Result resolver: translates Python-side JSON/IPCPayloads into TestController updates (start/pass/fail/output/attachments). +- Debug launcher: prepares debug sessions and coordinates the debugger attach flow with the Python runner. + +## Key components (files and responsibilities) + +- Entrypoints + - `src/client/testing/testController/controller.ts` — `PythonTestController` (main orchestrator). + - `src/client/testing/serviceRegistry.ts` — DI/wiring for testing services. +- Workspace orchestration + - `src/client/testing/testController/workspaceTestAdapter.ts` — `WorkspaceTestAdapter` (provider-agnostic entry used by controller). +- **Project-based testing (multi-project workspaces)** + - `src/client/testing/testController/common/testProjectRegistry.ts` — `TestProjectRegistry` (manages project lifecycle, discovery, and nested project handling). + - `src/client/testing/testController/common/projectAdapter.ts` — `ProjectAdapter` interface (represents a single Python project with its own test infrastructure). + - `src/client/testing/testController/common/projectUtils.ts` — utilities for project ID generation, display names, and shared adapter creation. +- Provider adapters + - Unittest + - `src/client/testing/testController/unittest/testDiscoveryAdapter.ts` + - `src/client/testing/testController/unittest/testExecutionAdapter.ts` + - Pytest + - `src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts` + - `src/client/testing/testController/pytest/pytestExecutionAdapter.ts` +- Result resolution and helpers + - `src/client/testing/testController/common/resultResolver.ts` — `PythonResultResolver` (maps payload -> TestController updates). + - `src/client/testing/testController/common/testItemUtilities.ts` — helpers for TestItem lifecycle. + - `src/client/testing/testController/common/types.ts` — `ITestDiscoveryAdapter`, `ITestExecutionAdapter`, `ITestResultResolver`, `ITestDebugLauncher`. + - `src/client/testing/testController/common/debugLauncher.ts` — debug session creation helper. + - `src/client/testing/testController/common/utils.ts` — named-pipe helpers and command builders (`startDiscoveryNamedPipe`, etc.). +- Configuration + - `src/client/testing/common/testConfigurationManager.ts` — per-workspace test settings. + - `src/client/testing/configurationFactory.ts` — configuration service factory. +- Utilities & glue + - `src/client/testing/utils.ts` — assorted helpers used by adapters. + - Python-side scripts: `python_files/unittestadapter/*`, `python_files/pytestadapter/*` — discovery/run code executed by adapters. + +## Python subprocess runners (what runs inside Python) + +The adapters in the extension don't implement test discovery/run logic themselves — they spawn a Python subprocess that runs small helper scripts located under `python_files/` and stream structured events back to the extension over the named-pipe IPC. This is a central part of the feature area; changes here usually require coordinated edits in both the TypeScript adapters and the Python scripts. + +- Unittest helpers (folder: `python_files/unittestadapter`) + + - `discovery.py` — performs `unittest` discovery and emits discovery payloads (test suites, cases, locations) on the IPC channel. + - `execution.py` / `django_test_runner.py` — run tests for `unittest` and, where applicable, Django test runners; emit run events (start, stdout/stderr, pass, fail, skip, teardown) and attachment info. + - `pvsc_utils.py`, `django_handler.py` — utility helpers used by the runners for environment handling and Django-specific wiring. + - The adapter TypeScript files (`testDiscoveryAdapter.ts`, `testExecutionAdapter.ts`) construct the command line, start a named-pipe listener, and spawn these Python scripts using the extension's ExecutionFactory (activated interpreter) so the scripts execute inside the user's selected environment. + +- Pytest helpers (folder: `python_files/vscode_pytest`) + + - `_common.py` — shared helpers for pytest runner scripts. + - `run_pytest_script.py` — the primary pytest runner used for discovery and execution; emits the same structured IPC payloads the extension expects (discovery events and run events). + - The `pytest` execution adapter (`pytestExecutionAdapter.ts`) and discovery adapter build the CLI to run `run_pytest_script.py`, start the pipe, and translate incoming payloads via `PythonResultResolver`. + +- IPC contract and expectations + + - Adapters rely on a stable JSON payload contract emitted by the Python scripts: identifiers for tests, event types (discovered, collected, started, passed, failed, skipped), timings, error traces, and optional attachments (logs, captured stdout/stderr, file links). + - The extension maps these payloads to `TestItem`/`TestRun` updates via `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`). If you change payload shape, update the resolver and tests concurrently. + +- How the subprocess is started + - Execution adapters use the extension's `ExecutionFactory` (preferred) to get an activated interpreter and then spawn a child process that runs the helper script. The adapter will set up environment variables and command-line args (including the pipe name / run-id) so the Python runner knows where to send events and how to behave (discovery vs run vs debug). + - For debug sessions a debug-specific entry argument/port is passed and `common/debugLauncher.ts` coordinates starting a VS Code debug session that will attach to the Python process. + +## Core functionality (what to change where) + +- Discovery + - Entry: `WorkspaceTestAdapter.discoverTests` → provider discovery adapter. Adapter starts a named-pipe listener, spawns the discovery script in an activated interpreter, forwards discovery events to `PythonResultResolver` which creates/updates TestItems. + - Files: `workspaceTestAdapter.ts`, `*DiscoveryAdapter.ts`, `resultResolver.ts`, `testItemUtilities.ts`. +- Run / Execution + - Entry: `WorkspaceTestAdapter.executeTests` → provider execution adapter. Adapter spawns runner in an activated env, runner streams run events to the pipe, `PythonResultResolver` updates a `TestRun` with start/pass/fail and attachments. + - Files: `workspaceTestAdapter.ts`, `*ExecutionAdapter.ts`, `resultResolver.ts`. +- Debugging + - Flow: debug request flows like a run but goes through `debugLauncher.ts` to create a VS Code debug session with prepared ports/pipes. The Python runner coordinates attach/continue with the debugger. + - Files: `*ExecutionAdapter.ts`, `common/debugLauncher.ts`, `common/types.ts`. +- Result reporting + - `resultResolver.ts` is the canonical place to change how JSON payloads map to TestController constructs (messages, durations, error traces, attachments). + +## Typical workflows (short) + +- Full discovery + + 1. `PythonTestController` triggers discovery -> `WorkspaceTestAdapter.discoverTests`. + 2. Provider discovery adapter starts pipe and launches Python discovery script. + 3. Discovery events -> `PythonResultResolver` -> TestController tree updated. + +- Run tests + + 1. Controller collects TestItems -> creates `TestRun`. + 2. `WorkspaceTestAdapter.executeTests` delegates to execution adapter which launches the runner. + 3. Runner events arrive via pipe -> `PythonResultResolver` updates `TestRun`. + 4. On process exit the run is finalized. + +- Debug a test + 1. Debug request flows to execution adapter. + 2. Adapter prepares ports and calls `debugLauncher` to start a VS Code debug session with the run ID. + 3. Runner coordinates with the debugger; `PythonResultResolver` still receives and applies run events. + +## Tests and examples to inspect + +- Unit/integration tests for adapters and orchestration under `src/test/` (examples): + - `src/test/testing/common/testingAdapter.test.ts` + - `src/test/testing/testController/workspaceTestAdapter.unit.test.ts` + - `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` + - Adapter tests demonstrate expected telemetry, debug-launch payloads and result resolution. + +## History & evolution (brief) + +- Migration to TestController API: the code organizes around VS Code TestController, mapping legacy adapter behaviour into TestItems/TestRuns. +- Named-pipe IPC: discovery/run use named-pipe IPC to stream events from Python runner scripts (`python_files/*`) which enables richer, incremental updates and debug coordination. +- Environment activation: adapters prefer the extension ExecutionFactory (activated interpreter) to run discovery and test scripts. + +## Pointers for contributors (practical) + +- To extend discovery output: update the Python discovery script in `python_files/*` and `resultResolver.ts` to parse new payload fields. +- To change run behaviour (args/env/timouts): update the provider execution adapter (`*ExecutionAdapter.ts`) and add/update tests under `src/test/`. +- To change debug flow: edit `common/debugLauncher.ts` and adapters' debug paths; update tests that assert launch argument shapes. + +## Django support (how it works) + +- The extension supports Django projects by delegating discovery and execution to Django-aware Python helpers under `python_files/unittestadapter`. + - `python_files/unittestadapter/django_handler.py` contains helpers that invoke `manage.py` for discovery or execute Django test runners inside the project context. + - `python_files/unittestadapter/django_test_runner.py` provides `CustomDiscoveryTestRunner` and `CustomExecutionTestRunner` which integrate with the extension by using the same IPC contract (they use `UnittestTestResult` and `send_post_request` to emit discovery/run payloads). +- How adapters pass Django configuration: + - Execution adapters set environment variables (e.g. `MANAGE_PY_PATH`) and modify `PYTHONPATH` so Django code and the custom test runner are importable inside the spawned subprocess. + - For discovery the adapter may run the discovery helper which calls `manage.py test` with a custom test runner that emits discovery payloads instead of executing tests. +- Practical notes for contributors: + - Changes to Django discovery/execution often require edits in both `django_test_runner.py`/`django_handler.py` and the TypeScript adapters (`testDiscoveryAdapter.ts` / `testExecutionAdapter.ts`). + - The Django test runner expects `TEST_RUN_PIPE` environment variable to be present to send IPC events (see `django_test_runner.py`). + +## Settings referenced by this feature area + +- The extension exposes several `python.testing.*` settings used by adapters and configuration code (declared in `package.json`): + - `python.testing.pytestEnabled`, `python.testing.unittestEnabled` — enable/disable frameworks. + - `python.testing.pytestPath`, `python.testing.pytestArgs`, `python.testing.unittestArgs` — command path and CLI arguments used when spawning helper scripts. + - `python.testing.cwd` — optional working directory used when running discovery/runs. + - `python.testing.autoTestDiscoverOnSaveEnabled`, `python.testing.autoTestDiscoverOnSavePattern` — control automatic discovery on save. + - `python.testing.debugPort` — default port used for debug runs. + - `python.testing.promptToConfigure` — whether to prompt users to configure tests when potential test folders are found. +- Where to look in the code: + - Settings are consumed by `src/client/testing/common/testConfigurationManager.ts`, `src/client/testing/configurationFactory.ts`, and adapters under `src/client/testing/testController/*` which read settings to build CLI args and env for subprocesses. + - The setting definitions and descriptions are in `package.json` and localized strings in `package.nls.json`. + +## Project-based testing (multi-project workspaces) + +Project-based testing enables multi-project workspace support where each Python project gets its own test tree root with its own Python environment. + +### Architecture + +- **TestProjectRegistry** (`testProjectRegistry.ts`): Central registry that: + + - Discovers Python projects via the Python Environments API + - Creates and manages `ProjectAdapter` instances per workspace + - Computes nested project relationships and configures ignore lists + - Falls back to "legacy" single-adapter mode when API unavailable + +- **ProjectAdapter** (`projectAdapter.ts`): Interface representing a single project with: + - Project identity (ID, name, URI from Python Environments API) + - Python environment with execution details + - Test framework adapters (discovery/execution) + - Nested project ignore paths (for parent projects) + +### How it works + +1. **Activation**: When the extension activates, `PythonTestController` checks if the Python Environments API is available. +2. **Project discovery**: `TestProjectRegistry.discoverAndRegisterProjects()` queries the API for all Python projects in each workspace. +3. **Nested handling**: `configureNestedProjectIgnores()` identifies child projects and adds their paths to parent projects' ignore lists. +4. **Test discovery**: For each project, the controller calls `project.discoveryAdapter.discoverTests()` with the project's URI. The adapter sets `PROJECT_ROOT_PATH` environment variable for the Python runner. +5. **Python side**: + - For pytest: `get_test_root_path()` in `vscode_pytest/__init__.py` returns `PROJECT_ROOT_PATH` (if set) or falls back to `cwd`. + - For unittest: `discovery.py` uses `PROJECT_ROOT_PATH` as `top_level_dir` and `project_root_path` to root the test tree at the project directory. +6. **Test tree**: Each project gets its own root node in the Test Explorer, with test IDs scoped by project ID using the `@@vsc@@` separator (defined in `projectUtils.ts`). + +### Nested project handling: pytest vs unittest + +**pytest** supports the `--ignore` flag to exclude paths during test collection. When nested projects are detected, parent projects automatically receive `--ignore` flags for child project paths. This ensures each test appears under exactly one project in the test tree. + +**unittest** does not support path exclusion during `discover()`. Therefore, tests in nested project directories may appear under multiple project roots (both the parent and the child project). This is **expected behavior** for unittest: + +- Each project discovers and displays all tests it finds within its directory structure +- There is no deduplication or collision detection +- Users may see the same test file under multiple project roots if their project structure has nesting + +This approach was chosen because: + +1. unittest's `TestLoader.discover()` has no built-in path exclusion mechanism +2. Implementing custom exclusion would add significant complexity with minimal benefit +3. The existing approach is transparent and predictable - each project shows what it finds + +### Empty projects and root nodes + +If a project discovers zero tests, its root node will still appear in the Test Explorer as an empty folder. This ensures consistent behavior and makes it clear which projects were discovered, even if they have no tests yet. + +### Logging prefix + +All project-based testing logs use the `[test-by-project]` prefix for easy filtering in the output channel. + +### Key files + +- Python side: + - `python_files/vscode_pytest/__init__.py` — `get_test_root_path()` function and `PROJECT_ROOT_PATH` environment variable for pytest. + - `python_files/unittestadapter/discovery.py` — `discover_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest discovery. + - `python_files/unittestadapter/execution.py` — `run_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest execution. +- TypeScript: `testProjectRegistry.ts`, `projectAdapter.ts`, `projectUtils.ts`, and the discovery/execution adapters. + +### Tests + +- `src/test/testing/testController/common/testProjectRegistry.unit.test.ts` — TestProjectRegistry tests +- `src/test/testing/testController/common/projectUtils.unit.test.ts` — Project utility function tests +- `python_files/tests/pytestadapter/test_discovery.py` — pytest PROJECT_ROOT_PATH tests (see `test_project_root_path_env_var()` and `test_symlink_with_project_root_path()`) +- `python_files/tests/unittestadapter/test_discovery.py` — unittest `project_root_path` / PROJECT_ROOT_PATH discovery tests +- `python_files/tests/unittestadapter/test_execution.py` — unittest `project_root_path` / PROJECT_ROOT_PATH execution tests +- `src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts` — unittest discovery adapter PROJECT_ROOT_PATH tests +- `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` — unittest execution adapter PROJECT_ROOT_PATH tests + +## Coverage support (how it works) + +- Coverage is supported by running the Python helper scripts with coverage enabled and then collecting a coverage payload from the runner. + - Pytest-side coverage logic lives in `python_files/vscode_pytest/__init__.py` (checks `COVERAGE_ENABLED`, imports `coverage`, computes per-file metrics and emits a `CoveragePayloadDict`). + - Unittest adapters enable coverage by setting environment variable(s) (e.g. `COVERAGE_ENABLED`) when launching the subprocess; adapters and `resultResolver.ts` handle the coverage profile kind (`TestRunProfileKind.Coverage`). +- Flow summary: + 1. User starts a Coverage run via Test Explorer (profile kind `Coverage`). + 2. Controller/adapters set `COVERAGE_ENABLED` (or equivalent) in the subprocess env and invoke the runner script. + 3. The Python runner collects coverage (using `coverage` or `pytest-cov`), builds a file-level coverage map, and sends a coverage payload back over the IPC. + 4. `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`) receives the coverage payload and stores `detailedCoverageMap` used by the TestController profile to show file-level coverage details. +- Tests that exercise coverage flows are under `src/test/testing/*` and `python_files/tests/*` (see `testingAdapter.test.ts` and adapter unit tests that assert `COVERAGE_ENABLED` is set appropriately). + +## Interaction with the VS Code API + +- TestController API + - The feature area is built on VS Code's TestController/TestItem/TestRun APIs (`vscode.tests.createTestController` / `tests.createTestController` in the code). The controller creates a `TestController` in `src/client/testing/testController/controller.ts` and synchronizes `TestItem` trees with discovery payloads. + - `PythonResultResolver` maps incoming JSON events to VS Code API calls: `testRun.appendOutput`, `testRun.passed/failed/skipped`, `testRun.end`, and `TestItem` updates (labels, locations, children). +- Debug API + - Debug runs use the Debug API to start an attach/launch session. The debug launcher implementation is in `src/client/testing/testController/common/debugLauncher.ts` which constructs a debug configuration and calls the VS Code debug API to start a session (e.g. `vscode.debug.startDebugging`). + - Debug adapter/resolver code in the extension's debugger modules may also be used when attaching to Django or test subprocesses. +- Commands and configuration + - The Test Controller wires commands that appear in the Test Explorer and editor context menus (see `package.json` contributes `commands`) and listens to configuration changes filtered by `python.testing` in `src/client/testing/main.ts`. +- The "Copy Test ID" command (`python.copyTestId`) can be accessed from both the Test Explorer context menu (`testing/item/context`) and the editor gutter icon context menu (`testing/item/gutter`). This command copies test identifiers to the clipboard in the appropriate format for the active test framework (pytest path format or unittest module.class.method format). +- Execution factory & activated environments + - Adapters use the extension `ExecutionFactory` to spawn subprocesses in an activated interpreter (so the user's venv/conda is used). This involves the extension's internal environment execution APIs and sometimes `envExt` helpers when the external environment extension is present. + +## Learnings + +- Never await `showErrorMessage()` calls in test execution adapters as it blocks the test UI thread and freezes the Test Explorer (1) +- VS Code test-related context menus are contributed to using both `testing/item/context` and `testing/item/gutter` menu locations in package.json for full coverage (1) + +``` + +``` diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 16fbb5233be9..000000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,2 +0,0 @@ -daysUntilLock: 7 -lockComment: false diff --git a/.github/prompts/extract-impl-instructions.prompt.md b/.github/prompts/extract-impl-instructions.prompt.md new file mode 100644 index 000000000000..c2fb08b443c7 --- /dev/null +++ b/.github/prompts/extract-impl-instructions.prompt.md @@ -0,0 +1,79 @@ +--- +mode: edit +--- + +Analyze the specified part of the VS Code Python Extension codebase to generate or update implementation instructions in `.github/instructions/.instructions.md`. + +## Task + +Create concise developer guidance focused on: + +### Implementation Essentials + +- **Core patterns**: How this component is typically implemented and extended +- **Key interfaces**: Essential classes, services, and APIs with usage examples +- **Integration points**: How this component interacts with other extension parts +- **Common tasks**: Typical development scenarios with step-by-step guidance + +### Content Structure + +````markdown +--- +description: 'Implementation guide for the part of the Python Extension' +--- + +# Implementation Guide + +## Overview + +Brief description of the component's purpose and role in VS Code Python Extension. + +## Key Concepts + +- Main abstractions and their responsibilities +- Important interfaces and base classes + +## Common Implementation Patterns + +### Pattern 1: [Specific Use Case] + +```typescript +// Code example showing typical implementation +``` +```` + +### Pattern 2: [Another Use Case] + +```typescript +// Another practical example +``` + +## Integration Points + +- How this component connects to other VS Code Python Extension systems +- Required services and dependencies +- Extension points and contribution models + +## Essential APIs + +- Key methods and interfaces developers need +- Common parameters and return types + +## Gotchas and Best Practices + +- Non-obvious behaviors to watch for +- Performance considerations +- Common mistakes to avoid + +``` + +## Guidelines +- **Be specific**: Use actual class names, method signatures, and file paths +- **Show examples**: Include working code snippets from the codebase +- **Target implementation**: Focus on how to build with/extend this component +- **Keep it actionable**: Every section should help developers accomplish tasks + +Source conventions from existing `.github/instructions/*.instructions.md`, `CONTRIBUTING.md`, and codebase patterns. + +If `.github/instructions/.instructions.md` exists, intelligently merge new insights with existing content. +``` diff --git a/.github/prompts/extract-usage-instructions.prompt.md b/.github/prompts/extract-usage-instructions.prompt.md new file mode 100644 index 000000000000..ea48f162a220 --- /dev/null +++ b/.github/prompts/extract-usage-instructions.prompt.md @@ -0,0 +1,30 @@ +--- +mode: edit +--- + +Analyze the user requested part of the codebase (use a suitable ) to generate or update `.github/instructions/.instructions.md` for guiding developers and AI coding agents. + +Focus on practical usage patterns and essential knowledge: + +- How to use, extend, or integrate with this code area +- Key architectural patterns and conventions specific to this area +- Common implementation patterns with code examples +- Integration points and typical interaction patterns with other components +- Essential gotchas and non-obvious behaviors + +Source existing conventions from `.github/instructions/*.instructions.md`, `CONTRIBUTING.md`, and `README.md`. + +Guidelines: + +- Write concise, actionable instructions using markdown structure +- Document discoverable patterns with concrete examples +- If `.github/instructions/.instructions.md` exists, merge intelligently +- Target developers who need to work with or extend this code area + +Update `.github/instructions/.instructions.md` with header: + +``` +--- +description: "How to work with the part of the codebase" +--- +``` diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000000..0058580e92e0 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,19 @@ +changelog: + exclude: + labels: + - 'no-changelog' + authors: + - 'dependabot' + + categories: + - title: Enhancements + labels: + - 'feature-request' + + - title: Bug Fixes + labels: + - 'bug' + + - title: Code Health + labels: + - 'debt' diff --git a/.github/release_plan.md b/.github/release_plan.md index 4ab57dbd9e1a..091ed559825b 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -1,91 +1,138 @@ -# 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 (not just to team but also to debugger and language server) -- [ ] Update master for the release - - [ ] Create a branch against `master` for a pull request - - [ ] Change the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) from a `-dev` suffix to `-rc` (🤖) - - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/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). - - [ ] Update [`CHANGELOG.md`](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) (🤖) - - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/master/news) (typically `python news --final --update CHANGELOG.md | code-insiders -`) - - [ ] Copy over the "Thanks" section from the previous release - - [ ] Make sure the "Thanks" section is up-to-date (e.g. compare to versions in requirements.json) - - [ ] 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/master/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/master/ThirdPartyNotices-Repository.txt) as appropriate - - [ ] Create a pull request against `master` (🤖) - - [ ] Merge pull request into `master` -- [ ] Update the [`release` branch](https://github.com/microsoft/vscode-python/branches) - - [ ] Delete the `release` branch in the repo - - [ ] Create a new `release` branch from `master` - - (alternately, force-push the master branch to the GitHub "release" branch) - - [ ] (if necessary) Request that the branch be set anew as "protected" -- [ ] Update master post-release (🤖) - - [ ] Bump the version number to the next monthly ("YYYY.M.0-dev") release in the `master` branch - - [ ] `package.json` - - [ ] `package-lock.json` - - [ ] Create a pull request against `master` - - [ ] Merge pull request into `master` -- [ ] Announce the code freeze is over -- [ ] Update [Component Governance](https://dev.azure.com/ms/vscode-python/_componentGovernance) (Click on "microsoft/vscode-python" on that page) - - [ ] Provide details for any automatically detected npm dependencies - - [ ] Manually add any repository dependencies -- [ ] GDPR bookkeeping (@brettcannon) (🤖; see team notes) -- [ ] 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 -- [ ] Begin drafting a [blog](http://aka.ms/pythonblog) post -- [ ] 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` branch - - [ ] Create a branch against `release` for a pull request - - [ ] Update the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) (🤖) - - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/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/master/CHANGELOG.md) (🤖) - - [ ] Update version and date for the release section - - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/master/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/master/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/master/ThirdPartyNotices-Repository.txt) manually if necessary - - [ ] Create pull request against `release` (🤖) - - [ ] Merge pull request into `release` -- [ ] Make sure component governance is happy - -## Release - -- [ ] Publish the release via Azure DevOps - - [ ] Make sure [CI](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md) is passing - - [ ] Make sure the "Upload" stage on the release page succeeded - - [ ] Make sure no extraneous files are being included in the `.vsix` file (make sure to check for hidden files) - - [ ] Deploy the "Publish" stage -- [ ] Create a [GitHub release](https://github.com/microsoft/vscode-python/releases) - - [ ] Have tag match the released version - - [ ] Copy the changelog entry for 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` back into `master` (🤖) - -## 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 (🤖) +### General Notes +All dates should align with VS Code's [iteration](https://github.com/microsoft/vscode/labels/iteration-plan) and [endgame](https://github.com/microsoft/vscode/labels/endgame-plan) plans. + +Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commits to `main` should only be in response to bugs found during endgame testing until the release candidate is ready. + +
+ Release Primary and Secondary Assignments for the 2025 Calendar Year + +| Month and version number | Primary | Secondary | +|------------|----------|-----------| +| January v2025.0.0 | Eleanor | Karthik | +| February v2025.2.0 | Anthony | Eleanor | +| March v2025.4.0 | Karthik | Anthony | +| April v2025.6.0 | Eleanor | Karthik | +| May v2025.8.0 | Anthony | Eleanor | +| June v2025.10.0 | Karthik | Anthony | +| July v2025.12.0 | Eleanor | Karthik | +| August v2025.14.0 | Anthony | Eleanor | +| September v2025.16.0 | Karthik | Anthony | +| October v2025.18.0 | Eleanor | Karthik | +| November v2025.20.0 | Anthony | Eleanor | +| December v2025.22.0 | Karthik | Anthony | + +
+ + +# Release candidate (Thursday, XXX XX) +NOTE: This Thursday occurs during TESTING week. Branching should be done during this week to freeze the release with only the correct changes. Any last minute fixes go in as candidates into the release branch and will require team approval. + +Other: +NOTE: Third Party Notices are automatically added by our build pipelines using https://tools.opensource.microsoft.com/notice. +NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. + + +### Step 1: +##### Bump the version of `main` to be a release candidate (also updating third party notices, and package-lock.json).❄️ (steps with ❄️ will dictate this step happens while main is frozen 🥶) + +- [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. +- [ ] Create a new branch called **`bump-release-[YYYY.minor]`**. +- [ ] Update `pet`: + - [ ] Go to the [pet](https://github.com/microsoft/python-environment-tools) repo and check `main` and latest `release/*` branch. If there are new changes in `main` then create a branch called `release/YYYY.minor` (matching python extension release `major.minor`). + - [ ] Update `build\azure-pipeline.stable.yml` to point to the latest `release/YYYY.minor` for `python-environment-tools`. +- [ ] Change the version in `package.json` to the next **even** number. (🤖) +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` at this point which update the version number **only**)_. (🤖) +- [ ] Update `ThirdPartyNotices-Repository.txt` as appropriate. You can check by looking at the [commit history](https://github.com/microsoft/vscode-python/commits/main) and scrolling through to see if there's anything listed there which might have pulled in some code directly into the repository from somewhere else. If you are still unsure you can check with the team. +- [ ] Create a PR from your branch **`bump-release-[YYYY.minor]`** to `main`. Add the `"no change-log"` tag to the PR so it does not show up on the release notes before merging it. + +NOTE: this PR will fail the test in our internal release pipeline called `VS Code (pre-release)` because the version specified in `main` is (temporarily) an invalid pre-release version. This is expected as this will be resolved below. + + +### Step 2: Creating your release branch ❄️ +- [ ] Create a release branch by creating a new branch called **`release/YYYY.minor`** branch from `main`. This branch is now the candidate for our release which will be the base from which we will release. + +NOTE: If there are release branches that are two versions old you can delete them at this time. + +### Step 3 Create a draft GitHub release for the release notes (🤖) ❄️ + +- [ ] Create a new [GitHub release](https://github.com/microsoft/vscode-python/releases/new). +- [ ] Specify a new tag called `YYYY.minor.0`. +- [ ] Have the `target` for the github release be your release branch called **`release/YYYY.minor`**. +- [ ] Create the release notes by specifying the previous tag for the last stable release and click `Generate release notes`. Quickly check that it only contain notes from what is new in this release. +- [ ] Click `Save draft`. + +### Step 4: Return `main` to dev and unfreeze (❄️ ➡ 💧) +NOTE: The purpose of this step is ensuring that main always is on a dev version number for every night's 🌃 pre-release. Therefore it is imperative that you do this directly after the previous steps to reset the version in main to a dev version **before** a pre-release goes out. +- [ ] Create a branch called **`bump-dev-version-YYYY.[minor+1]`**. +- [ ] Bump the minor version number in the `package.json` to the next `YYYY.[minor+1]` which will be an odd number, and add `-dev`.(🤖) +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (🤖) +- [ ] Create a PR from this branch against `main` and merge it. + +NOTE: this PR should make all CI relating to `main` be passing again (such as the failures stemming from step 1). + +### Step 5: Notifications and Checks on External Release Factors +- [ ] Check [Component Governance](https://dev.azure.com/monacotools/Monaco/_componentGovernance/192726?_a=alerts&typeId=11825783&alerts-view-option=active) to make sure there are no active alerts. +- [ ] Manually add/fix any 3rd-party licenses as appropriate based on what the internal build pipeline detects. +- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython). +- [ ] Contact the PM team to begin drafting a blog post. +- [ ] Announce to the development team that `main` is open again. + + +# Release (Wednesday, XXX XX) + +### Step 6: Take the release branch from a candidate to the finalized release +- [ ] 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. +- [ ] Check to make sure any final updates to the **`release/YYYY.minor`** branch have been merged. + +### Step 7: Execute the Release +- [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (🤖). +- [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the **`release/YYYY.minor`** branch. + - [ ] Click `run pipeline`. + - [ ] for `branch/tag` select the release branch which is **`release/YYYY.minor`**. + - NOTE: Please opt to release the python extension close to when VS Code is released to align when release notes go out. When we bump the VS Code engine number, our extension will not go out to stable until the VS Code stable release but this only occurs when we bump the engine number. +- [ ] 🧍🧍 Get approval on the release on the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299). +- [ ] Click "approve" in the publish step of [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) to publish the release to the marketplace. 🎉 +- [ ] Take the Github release out of draft. +- [ ] Publish documentation changes. +- [ ] Contact the PM team to publish the blog post. +- [ ] Determine if a hotfix is needed. +- [ ] Merge the release branch **`release/YYYY.minor`** back into `main`. (This step is only required if changes were merged into the release branch. If the only change made on the release branch is the version, this is not necessary. Overall you need to ensure you DO NOT overwrite the version on the `main` branch.) + + +## Steps for Point Release (if necessary) +- [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. +- [ ] checkout to the `release/YYY.minor` and check to make sure all necessary changes for the point release have been cherry-picked into the release branch. If not, contact the owner of the changes to do so. +- [ ] Create a branch against **`release/YYYY.minor`** called **`release-[YYYY.minor.point]`**. +- [ ] Bump the point version number in the `package.json` to the next `YYYY.minor.point` +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (🤖) +- [ ] If Point Release is due to an issue in `pet`. Update `build\azure-pipeline.stable.yml` to point to the branch `release/YYYY.minor` for `python-environment-tools` with the fix or decided by the team. +- [ ] Create a PR from this branch against `release/YYYY.minor` +- [ ] **Rebase** and merge this PR into the release branch +- [ ] Create a draft GitHub release for the release notes (🤖) ❄️ + - [ ] Create a new [GitHub release](https://github.com/microsoft/vscode-python/releases/new). + - [ ] Specify a new tag called `vYYYY.minor.point`. + - [ ] Have the `target` for the github release be your release branch called **`release/YYYY.minor`**. + - [ ] Create the release notes by specifying the previous tag as the previous version of stable, so the minor release **`vYYYY.minor`** for the last stable release and click `Generate release notes`. + - [ ] Check the generated notes to ensure that all PRs for the point release are included so users know these new changes. + - [ ] Click `Save draft`. +- [ ] Publish the point release + - [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (🤖). + - [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the **`release/YYYY.minor`** branch. + - [ ] Click `run pipeline`. + - [ ] for `branch/tag` select the release branch which is **`release/YYYY.minor`**. + - [ ] 🧍🧍 Get approval on the release on the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) and publish the release to the marketplace. 🎉 + - [ ] Take the Github release out of draft. + +## Steps for contributing to a point release +- [ ] Work with team to decide if point release is necessary +- [ ] Work with team or users to verify the fix is correct and solves the problem without creating any new ones +- [ ] Create PR/PRs and merge then each into main as usual +- [ ] Make sure to still mark if the change is "bug" or "no-changelog" +- [ ] Cherry-pick all PRs to the release branch and check that the changes are in before the package is bumped +- [ ] Notify the release champ that your changes are in so they can trigger a point-release + ## Prep for the _next_ release -- [ ] Create a new [release plan](https://raw.githubusercontent.com/microsoft/vscode-python/master/.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) (🤖) +- [ ] 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-plan) (🤖) diff --git a/.github/test_plan.md b/.github/test_plan.md deleted file mode 100644 index 161f301f983d..000000000000 --- a/.github/test_plan.md +++ /dev/null @@ -1,630 +0,0 @@ -# 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 blacklisted 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/master/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/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000000..09d019dec4a7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,449 @@ +name: Build + +on: + push: + branches: + - 'main' + - 'release' + - 'release/*' + - 'release-*' + +permissions: {} + +env: + NODE_VERSION: 22.21.1 + PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 + # Force a path with spaces and to test extension works in these scenarios + # Unicode characters are causing 2.7 failures so skip that for now. + special-working-directory: './path with spaces' + special-working-directory-relative: 'path with spaces' + # 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. + MOCHA_REPORTER_JUNIT: true + +jobs: + setup: + name: Set up + if: github.repository == 'microsoft/vscode-python' + runs-on: ubuntu-latest + defaults: + run: + shell: python + outputs: + vsix_basename: ${{ steps.vsix_names.outputs.vsix_basename }} + vsix_name: ${{ steps.vsix_names.outputs.vsix_name }} + vsix_artifact_name: ${{ steps.vsix_names.outputs.vsix_artifact_name }} + steps: + - name: VSIX names + id: vsix_names + run: | + import os + if os.environ["GITHUB_REF"].endswith("/main"): + vsix_type = "insiders" + else: + vsix_type = "release" + print(f"::set-output name=vsix_name::ms-python-{vsix_type}.vsix") + print(f"::set-output name=vsix_basename::ms-python-{vsix_type}") + print(f"::set-output name=vsix_artifact_name::ms-python-{vsix_type}-vsix") + + build-vsix: + name: Create VSIX + if: github.repository == 'microsoft/vscode-python' + needs: setup + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + vsix-target: win32-x64 + - os: windows-latest + target: aarch64-pc-windows-msvc + vsix-target: win32-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: linux-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-gnu + # vsix-target: linux-arm64 + # - os: ubuntu-latest + # target: arm-unknown-linux-gnueabihf + # vsix-target: linux-armhf + # - os: macos-latest + # target: x86_64-apple-darwin + # vsix-target: darwin-x64 + # - os: macos-14 + # target: aarch64-apple-darwin + # vsix-target: darwin-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: alpine-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-musl + # vsix-target: alpine-arm64 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: 'python-env-tools' + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Build VSIX + uses: ./.github/actions/build-vsix + with: + node_version: ${{ env.NODE_VERSION}} + vsix_name: ${{ needs.setup.outputs.vsix_basename }}-${{ matrix.vsix-target }}.vsix + artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }}-${{ matrix.vsix-target }} + cargo_target: ${{ matrix.target }} + vsix_target: ${{ matrix.vsix-target }} + + lint: + name: Lint + if: github.repository == 'microsoft/vscode-python' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Lint + uses: ./.github/actions/lint + with: + node_version: ${{ env.NODE_VERSION }} + + check-types: + name: Check Python types + if: github.repository == 'microsoft/vscode-python' + runs-on: ubuntu-latest + steps: + - name: Use Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install core Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + options: '-t ./python_files/lib/python --no-cache-dir --implementation py' + + - name: Install Jedi requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --no-cache-dir --implementation py' + + - name: Install other Python requirements + run: | + python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Pyright + uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 + with: + version: 1.1.308 + working-directory: 'python_files' + + python-tests: + name: Python 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 }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + 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: ['3.10', '3.x', '3.13'] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python python_files/tests/run_all.py + + tests: + name: Tests + if: github.repository == 'microsoft/vscode-python' + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + 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.x'] + test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: ${{ env.special-working-directory-relative }}/package-lock.json + + - name: Install dependencies (npm ci) + run: npm ci + + - name: Compile + run: npx gulp prePublishNonBundle + + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + + - name: Install Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Upgrade Pip + run: python -m pip install -U pip + + # For faster/better builds of sdists. + - name: Install build pre-requisite + run: python -m pip install wheel nox + + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + + - name: Install functional test requirements + run: 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 poetry for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install poetry + Move-Item -Path ".\build\ci\pyproject.toml" -Destination . + poetry env use python + + - 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 `*.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 + & $condaExecPath init --all + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "CI_PYTHON_PATH=python" >> $GITHUB_ENV + echo "CI_DISABLE_AUTO_SELECTION=1" >> $GITHUB_ENV + 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 + if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, '3.') + + # 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@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'venv' && matrix.os == 'ubuntu-latest' + + - name: Run single-workspace tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'single-workspace' + + - name: Run multi-workspace tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testMultiWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'multi-workspace' + + - name: Run debugger tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testDebugger + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'debugger' + + # Run TypeScript functional tests + - name: Run TypeScript functional tests + run: npm run test:functional + if: matrix.test-suite == 'functional' + + smoke-tests: + name: Smoke tests + if: github.repository == 'microsoft/vscode-python' + runs-on: ${{ matrix.os }} + needs: [setup, 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. + include: + - os: windows-latest + vsix-target: win32-x64 + - os: ubuntu-latest + vsix-target: linux-x64 + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Smoke tests + uses: ./.github/actions/smoke-tests + with: + node_version: ${{ env.NODE_VERSION }} + artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }}-${{ matrix.vsix-target }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index a16f010a30a2..000000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,564 +0,0 @@ -name: CI - -on: - push: - branches: - # Run the CI workflow only on master for microsoft/vscode-python for now. - - master - -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 - COVERAGE_REPORTS: tests-coverage-reports - TEST_RESULTS_DIRECTORY: . - LKG_TAG: ci-lkg - -jobs: - ### Initialization: retrieve, install and cache dependencies - python-deps: - name: Install Python Requirements - runs-on: ubuntu-latest - if: github.repository == 'microsoft/vscode-python' - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Show all env vars - run: | - printenv - shell: bash - - - 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: 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 - - - name: Install debugpy with wheels - 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 - - # Retrieve the list of debugpy versions from PyPI in order to increase the specificity of the pip cache key, - # so that the cache gets invalidated as necessary. - # See https://github.com/microsoft/vscode-python/pull/9843#discussion_r373635221 - - name: curl PyPI to get debugpy versions - run: curl --output debugpy.json https://pypi.org/pypi/debugpy/json - - - name: Cache pip files - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-pip-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}}-${{hashFiles('debugpy.json')}} - - - name: Cache ./pythonFiles/lib/python - uses: actions/cache@v1 - with: - path: ./pythonFiles/lib/python - key: ${{runner.os}}-${{env.CACHE_PYTHONFILES}}-pythonFiles-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}}-${{hashFiles('debugpy.json')}} - - js-ts-deps: - name: Install npm dependencies - runs-on: ubuntu-latest - if: github.repository == 'microsoft/vscode-python' - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Install dependencies (npm ci) - run: npm ci --prefer-offline - - - name: Cache npm files - uses: actions/cache@v1 - with: - path: ~/.npm - key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} - - ### Hygiene + VSIX upload - compile-hygiene: - name: Compile, lint, check for errors - runs-on: ubuntu-latest - if: github.repository == 'microsoft/vscode-python' - needs: [js-ts-deps, python-deps] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Show all env vars - run: | - printenv - shell: bash - - - name: Retrieve cached npm files - uses: actions/cache@v1 - 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@v1 - 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 - - build-vsix: - name: Build VSIX - runs-on: ubuntu-latest - if: github.repository == 'microsoft/vscode-python' - needs: [python-deps, js-ts-deps] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Retrieve cached npm files - uses: actions/cache@v1 - with: - path: ~/.npm - key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} - - - name: curl PyPI to get debugpy versions - run: curl --output debugpy.json https://pypi.org/pypi/debugpy/json - - - name: Retrieve cached pythonFiles/ directory - uses: actions/cache@v1 - with: - path: ./pythonFiles/lib/python - key: ${{runner.os}}-${{env.CACHE_PYTHONFILES}}-pythonFiles-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}}-${{hashFiles('debugpy.json')}} - - - name: Install dependencies (npm ci) - run: npm ci --prefer-offline - - - name: Clean directory - run: npm run clean - - # 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. - - name: Update build number - run: npm run updateBuildNumber -- --buildNumber $GITHUB_RUN_ID - - - name: Package the VSIX - run: npm run package - - - uses: actions/upload-artifact@v1 - with: - name: ${{env.ARTIFACT_NAME_VSIX}} - path: ms-python-insiders.vsix - - ### 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' - needs: [python-deps, js-ts-deps] - 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: Show all env vars - run: | - printenv - shell: bash - - - name: Retrieve cached npm files - uses: actions/cache@v1 - with: - path: ~/.npm - key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} - - - name: Install dependencies (npm ci) - run: npm ci - - - name: Retrieve cached compile output directory - # 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@v1 - with: - path: ./out - key: ${{runner.os}}-${{env.CACHE_OUT_DIRECTORY}}-${{hashFiles('src/**')}} - - - 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@v1 - with: - node-version: ${{env.NODE_VERSION}} - - - name: curl PyPI to get debugpy versions - run: curl --output debugpy.json https://pypi.org/pypi/debugpy/json - - # We're intentionally not retrieving cached Python requirements installation, as it appears that pulling the cache pulls in some extra libraries as well, - # which causes problems with the tests. Also, running the installation seems much faster than retrieving it from cache. - - 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.0 - with: - run: npm run testSingleWorkspace - if: matrix.test-suite == 'venv' - - - name: Run single-workspace tests - uses: GabrielBB/xvfb-action@v1.0 - with: - run: npm run testSingleWorkspace - if: matrix.test-suite == 'single-workspace' - - - name: Run multi-workspace tests - uses: GabrielBB/xvfb-action@v1.0 - with: - run: npm run testMultiWorkspace - if: matrix.test-suite == 'multi-workspace' - - - name: Run debugger tests - uses: GabrielBB/xvfb-action@v1.0 - 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 }} - needs: [build-vsix] - 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] - python: [3.8] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Show all env vars - run: | - printenv - shell: bash - - - name: Use Python ${{matrix.python}} - uses: actions/setup-python@v2 - with: - python-version: ${{matrix.python}} - - - name: Retrieve cached npm files - uses: actions/cache@v1 - 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: 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 - - - name: Download VSIX - uses: actions/download-artifact@v1 - with: - name: ${{env.ARTIFACT_NAME_VSIX}} - - # Extract the artifact from its download folder (./${{env.ARTIFACT_NAME_VSIX}}) to the repo root, - # then delete the download folder and compile the source code. - - name: Prepare for smoke tests - run: | - mv ${{env.ARTIFACT_NAME_VSIX}}/* . - rm -r ${{env.ARTIFACT_NAME_VSIX}} - 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.0 - 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: Retrieve cached npm files - uses: actions/cache@v1 - 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: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - file: ./coverage/cobertura-coverage.xml - - lkg-tag: - # LKG = last known good - name: Tag successful build as CI LKG - runs-on: ubuntu-latest - needs: [tests, smoke-tests] - if: github.repository == 'microsoft/vscode-python' - steps: - - name: Delete existing tag - run: | - curl -s -X DELETE https://api.github.com/repos/microsoft/vscode-python/git/refs/tags/${{env.LKG_TAG}} \ - -H "Authorization: token ${{secrets.GITHUB_TOKEN}}" - - # We only need to create a tag reference for lightweight tags. - # See https://developer.github.com/v3/git/tags/#create-a-tag-object - # And https://developer.github.com/v3/git/refs/#create-a-reference - - name: Create a tag reference - run: | - curl -s -X POST "https://api.github.com/repos/microsoft/vscode-python/git/refs" \ - -H "Authorization: token ${{secrets.GITHUB_TOKEN}}" \ - -d @- << EOF - { - "ref": "refs/tags/${{env.LKG_TAG}}", - "sha": "${{github.sha}}" - } - EOF diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000000..5528fbbe9c0a --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: 'CodeQL' + +on: + push: + branches: + - main + - release-* + - release/* + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: '0 3 * * 0' + +permissions: + security-events: write + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['javascript', 'python'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + #- name: Autobuild + # uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml new file mode 100644 index 000000000000..27f93400a023 --- /dev/null +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -0,0 +1,28 @@ +name: Community Feedback Auto Comment + +on: + issues: + types: + - labeled +jobs: + add-comment: + if: github.event.label.name == 'needs community feedback' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Check For Existing Comment + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 + id: finder + with: + issue-number: ${{ github.event.issue.number }} + comment-author: 'github-actions[bot]' + body-includes: 'Thanks for the feature request! We are going to give the community' + + - name: Add Community Feedback Comment + if: steps.finder.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Thanks for the feature request! We are going to give the community 60 days from when this issue was created to provide 7 👍 upvotes on the opening comment to gauge general interest in this idea. If there's enough upvotes then we will consider this feature request in our future planning. If there's unfortunately not enough upvotes then we will close this issue. diff --git a/.github/workflows/gen-issue-velocity.yml b/.github/workflows/gen-issue-velocity.yml new file mode 100644 index 000000000000..41d79e4074d0 --- /dev/null +++ b/.github/workflows/gen-issue-velocity.yml @@ -0,0 +1,34 @@ +name: Issues Summary + +on: + schedule: + - cron: '0 0 * * 2' # Runs every Tuesday at midnight + workflow_dispatch: + +permissions: + issues: read + +jobs: + generate-summary: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests + + - name: Run summary script + run: python scripts/issue_velocity_summary_script.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml new file mode 100644 index 000000000000..46892a58e800 --- /dev/null +++ b/.github/workflows/info-needed-closer.yml @@ -0,0 +1,33 @@ +name: Info-Needed Closer +on: + schedule: + - cron: 20 12 * * * # 5:20am Redmond + repository_dispatch: + types: [trigger-needs-more-info] + workflow_dispatch: + +permissions: + issues: write + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v6 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + persist-credentials: false + ref: stable + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run info-needed Closer + uses: ./actions/needs-more-info-closer + with: + token: ${{secrets.GITHUB_TOKEN}} + label: info-needed + closeDays: 30 + closeComment: "Because we have not heard back with the information we requested, we are closing this issue for now. If you are able to provide the info later on, then we will be happy to re-open this issue to pick up where we left off. \n\nHappy Coding!" + pingDays: 30 + pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml new file mode 100644 index 000000000000..dcbd114086e2 --- /dev/null +++ b/.github/workflows/issue-labels.yml @@ -0,0 +1,34 @@ +name: Issue labels + +on: + issues: + types: [opened, reopened] + +env: + TRIAGERS: '["karthiknadig","eleanorjboyd","anthonykim1"]' + +permissions: + issues: write + +jobs: + # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. + add-classify-label: + name: "Add 'triage-needed' and remove assignees" + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v6 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + persist-credentials: false + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: "Add 'triage-needed' and remove assignees" + uses: ./actions/python-issue-labels + with: + triagers: ${{ env.TRIAGERS }} + token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml new file mode 100644 index 000000000000..544d04ee185e --- /dev/null +++ b/.github/workflows/lock-issues.yml @@ -0,0 +1,24 @@ +name: 'Lock Issues' + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + issues: write + +concurrency: + group: lock + +jobs: + lock-issues: + runs-on: ubuntu-latest + steps: + - name: 'Lock Issues' + uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 + with: + github-token: ${{ github.token }} + issue-inactive-days: '30' + process-only: 'issues' + log-output: true diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 000000000000..c8a6f2dd416e --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,689 @@ +name: PR/CI Check + +on: + pull_request: + push: + branches-ignore: + - main + - release* + +permissions: {} + +env: + NODE_VERSION: 22.21.1 + PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 + 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. + ARTIFACT_NAME_VSIX: ms-python-insiders-vsix + TEST_RESULTS_DIRECTORY: . + # Force a path with spaces and to test extension works in these scenarios + # Unicode characters are causing 2.7 failures so skip that for now. + special-working-directory: './path with spaces' + special-working-directory-relative: 'path with spaces' + +jobs: + build-vsix: + name: Create VSIX + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + vsix-target: win32-x64 + - os: windows-latest + target: aarch64-pc-windows-msvc + vsix-target: win32-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: linux-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-gnu + # vsix-target: linux-arm64 + # - os: ubuntu-latest + # target: arm-unknown-linux-gnueabihf + # vsix-target: linux-armhf + # - os: macos-latest + # target: x86_64-apple-darwin + # vsix-target: darwin-x64 + # - os: macos-14 + # target: aarch64-apple-darwin + # vsix-target: darwin-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: alpine-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-musl + # vsix-target: alpine-arm64 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: 'python-env-tools' + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Build VSIX + uses: ./.github/actions/build-vsix + with: + node_version: ${{ env.NODE_VERSION}} + vsix_name: 'ms-python-insiders-${{ matrix.vsix-target }}.vsix' + artifact_name: '${{ env.ARTIFACT_NAME_VSIX }}-${{ matrix.vsix-target }}' + cargo_target: ${{ matrix.target }} + vsix_target: ${{ matrix.vsix-target }} + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Lint + uses: ./.github/actions/lint + with: + node_version: ${{ env.NODE_VERSION }} + + check-types: + name: Check Python types + runs-on: ubuntu-latest + steps: + - name: Use Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: 'python-env-tools' + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + options: '-t ./python_files/lib/python --no-cache-dir --implementation py' + + - name: Install Jedi requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --no-cache-dir --implementation py' + + - name: Install other Python requirements + run: | + python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Pyright + uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 + with: + version: 1.1.308 + working-directory: 'python_files' + + python-tests: + name: Python 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 }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + 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: ['3.10', '3.x', '3.13'] # run for 3 pytest versions, most recent stable, oldest version supported and pre-release + pytest-version: ['pytest', 'pytest@pre-release', 'pytest==6.2.0'] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Install specific pytest version + if: matrix.pytest-version == 'pytest@pre-release' + run: | + python -m pip install --pre pytest + + - name: Install specific pytest version + if: matrix.pytest-version != 'pytest@pre-release' + run: | + python -m pip install "${{ matrix.pytest-version }}" + + - name: Install specific pytest version + run: python -m pytest --version + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python python_files/tests/run_all.py + + 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 }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + 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: ['3.x'] + test-suite: [ts-unit, venv, single-workspace, debugger, functional] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: ${{ env.special-working-directory-relative }}/package-lock.json + + - name: Install dependencies (npm ci) + run: npm ci + + - name: Compile + run: npx gulp prePublishNonBundle + + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Upgrade Pip + run: python -m pip install -U pip + + # For faster/better builds of sdists. + - name: Install build pre-requisite + run: python -m pip install wheel nox + + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + + - name: Install functional test requirements + run: 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 poetry for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install poetry + Move-Item -Path ".\build\ci\pyproject.toml" -Destination . + poetry env use python + + - 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 `*.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 + & $condaExecPath init --all + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "CI_PYTHON_PATH=python" >> $GITHUB_ENV + echo "CI_DISABLE_AUTO_SELECTION=1" >> $GITHUB_ENV + 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 + if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) + + # 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@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'venv' + + - name: Run single-workspace tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'single-workspace' + + - name: Run debugger tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testDebugger + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'debugger' + + # Run TypeScript functional tests + - name: Run TypeScript functional tests + run: npm run test:functional + if: matrix.test-suite == 'functional' + + native-tests: + name: Native 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 }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + 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] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Python Environment Tools tests + run: cargo test -- --nocapture + working-directory: ${{ env.special-working-directory }}/python-env-tools + + 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 }} + 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. + include: + - os: windows-latest + vsix-target: win32-x64 + - os: ubuntu-latest + vsix-target: linux-x64 + + steps: + # Need the source to have the tests available. + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Smoke tests + uses: ./.github/actions/smoke-tests + with: + node_version: ${{ env.NODE_VERSION }} + artifact_name: '${{ env.ARTIFACT_NAME_VSIX }}-${{ matrix.vsix-target }}' + + ### Coverage run + coverage: + name: Coverage + # TEMPORARILY DISABLED - hanging in CI, needs investigation + if: false + # 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 }} + needs: [lint, check-types, python-tests, tests, native-tests] + strategy: + fail-fast: false + matrix: + # Only run coverage on linux for PRs + os: [ubuntu-latest] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies (npm ci) + run: npm ci + + - name: Compile + run: npx gulp prePublishNonBundle + + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + + - name: Use Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: | + requirements.txt + python_files/jedilsp_requirements/requirements.txt + build/test-requirements.txt + build/functional-test-requirements.txt + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + options: '-t ./python_files/lib/python --implementation py' + + - name: Install Jedi requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --implementation py' + + - name: Install build pre-requisite + run: python -m pip install wheel nox + shell: bash + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Install functional test requirements + run: python -m pip install --upgrade -r ./build/functional-test-requirements.txt + + - name: Prepare pipenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + run: | + python -m pip install pipenv + python -m pipenv run python ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} pipenvPath + + - name: Prepare poetry for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + shell: pwsh + run: | + python -m pip install poetry + Move-Item -Path ".\build\ci\pyproject.toml" -Destination . + poetry env use python + + - name: Prepare virtualenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + 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 + 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 + run: | + # 1. For `*.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 + & $condaExecPath init --all + + - name: Run TypeScript unit tests + run: npm run test:unittests:cover + + - name: Run Python unit tests + run: | + python python_files/tests/run_all.py + + # 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: ${{ env.PYTHON_VERSION }} + CI_DISABLE_AUTO_SELECTION: 1 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace:cover + + - name: Run single-workspace tests + env: + CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} + CI_DISABLE_AUTO_SELECTION: 1 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace:cover + + # Enable these tests when coverage is setup for multiroot workspace tests + # - name: Run multi-workspace tests + # env: + # CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} + # CI_DISABLE_AUTO_SELECTION: 1 + # uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + # with: + # run: npm run testMultiWorkspace:cover + + # Enable these tests when coverage is setup for debugger tests + # - name: Run debugger tests + # env: + # CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} + # CI_DISABLE_AUTO_SELECTION: 1 + # uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + # with: + # run: npm run testDebugger:cover + + # Run TypeScript functional tests + - name: Run TypeScript functional tests + env: + CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} + CI_DISABLE_AUTO_SELECTION: 1 + run: npm run test:functional:cover + + - name: Generate coverage reports + run: npm run test:cover:report + + - name: Upload HTML report + uses: actions/upload-artifact@v7 + with: + name: ${{ runner.os }}-coverage-report-html + path: ./coverage + retention-days: 1 diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml new file mode 100644 index 000000000000..6364e5fa744e --- /dev/null +++ b/.github/workflows/pr-file-check.yml @@ -0,0 +1,44 @@ +name: PR files + +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'synchronize' + - 'labeled' + - 'unlabeled' + +permissions: {} + +jobs: + changed-files-in-pr: + name: 'Check for changed files' + runs-on: ubuntu-latest + steps: + - name: 'package-lock.json matches package.json' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 + with: + prereq-pattern: 'package.json' + file-pattern: 'package-lock.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' + + - name: 'package.json matches package-lock.json' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 + with: + prereq-pattern: 'package-lock.json' + file-pattern: 'package.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' + + - name: 'Tests' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 + with: + prereq-pattern: src/**/*.ts + file-pattern: | + src/**/*.test.ts + src/**/*.testvirtualenvs.ts + .github/test_plan.md + skip-label: 'skip tests' + failure-message: 'TypeScript code was edited without also editing a ${file-pattern} file; see the Testing page in our wiki on testing guidelines (the ${skip-label} label can be used to pass this check)' diff --git a/.github/workflows/pr-issue-check.yml b/.github/workflows/pr-issue-check.yml new file mode 100644 index 000000000000..5587227d2848 --- /dev/null +++ b/.github/workflows/pr-issue-check.yml @@ -0,0 +1,31 @@ +name: PR issue check + +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'synchronize' + - 'labeled' + - 'unlabeled' + +permissions: {} + +jobs: + check-for-attached-issue: + name: 'Check for attached issue' + runs-on: ubuntu-latest + steps: + - name: 'Ensure PR has an associated issue' + uses: actions/github-script@v9 + with: + script: | + const labels = context.payload.pull_request.labels.map(label => label.name); + if (!labels.includes('skip-issue-check')) { + const prBody = context.payload.pull_request.body || ''; + const issueLink = prBody.match(/https:\/\/github\.com\/\S+\/issues\/\d+/); + const issueReference = prBody.match(/#\d+/); + if (!issueLink && !issueReference) { + core.setFailed('No associated issue found in the PR description.'); + } + } diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 000000000000..af24ac10772c --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,24 @@ +name: 'PR labels' +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'labeled' + - 'unlabeled' + - 'synchronize' + +jobs: + classify: + name: 'Classify PR' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: 'PR impact specified' + uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2 + with: + mode: exactly + count: 1 + labels: 'bug, debt, feature-request, no-changelog' diff --git a/.github/workflows/python27-issue-response.yml b/.github/workflows/python27-issue-response.yml new file mode 100644 index 000000000000..9db84bca1a23 --- /dev/null +++ b/.github/workflows/python27-issue-response.yml @@ -0,0 +1,16 @@ +on: + issues: + types: [opened] + +jobs: + python27-issue-response: + runs-on: ubuntu-latest + permissions: + issues: write + if: "contains(github.event.issue.body, 'Python version (& distribution if applicable, e.g. Anaconda): 2.7')" + steps: + - name: Check for Python 2.7 string + run: | + response="We're sorry, but we no longer support Python 2.7. If you need to work with Python 2.7, you will have to pin to 2022.2.* version of the extension, which was the last version that had the debugger (debugpy) with support for python 2.7, and was tested with `2.7`. Thank you for your understanding! \n ![https://user-images.githubusercontent.com/51720070/80000627-39dacc00-8472-11ea-9755-ac7ba0acbb70.gif](https://user-images.githubusercontent.com/51720070/80000627-39dacc00-8472-11ea-9755-ac7ba0acbb70.gif)" + gh issue comment ${{ github.event.issue.number }} --body "$response" + gh issue close ${{ github.event.issue.number }} diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml new file mode 100644 index 000000000000..24352526d0d8 --- /dev/null +++ b/.github/workflows/remove-needs-labels.yml @@ -0,0 +1,20 @@ +name: 'Remove Needs Label' +on: + issues: + types: [closed] + +jobs: + classify: + name: 'Remove needs labels on issue closing' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: 'Removes needs labels on issue close' + uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 + with: + labels: | + needs PR + needs spike + needs community feedback + needs proposal diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml new file mode 100644 index 000000000000..57db4a3e18a7 --- /dev/null +++ b/.github/workflows/test-plan-item-validator.yml @@ -0,0 +1,30 @@ +name: Test Plan Item Validator +on: + issues: + types: [edited, labeled] + +permissions: + issues: write + +jobs: + main: + runs-on: ubuntu-latest + if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') + steps: + - name: Checkout Actions + uses: actions/checkout@v6 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + persist-credentials: false + ref: stable + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Run Test Plan Item Validator + uses: ./actions/test-plan-item-validator + with: + label: testplan-item + invalidLabel: invalid-testplan-item + comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml new file mode 100644 index 000000000000..c7a37ba0c78d --- /dev/null +++ b/.github/workflows/triage-info-needed.yml @@ -0,0 +1,57 @@ +name: Triage "info-needed" label + +on: + issue_comment: + types: [created] + +env: + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon","anthonykim1"]' + +jobs: + add_label: + if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Checkout Actions + uses: actions/checkout@v6 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + persist-credentials: false + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Add "info-needed" label + uses: ./actions/python-triage-info-needed + with: + triagers: ${{ env.TRIAGERS }} + action: 'add' + token: ${{secrets.GITHUB_TOKEN}} + + remove_label: + if: contains(github.event.issue.labels.*.name, 'info-needed') && contains(github.event.issue.labels.*.name, 'triage-needed') + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Checkout Actions + uses: actions/checkout@v6 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + persist-credentials: false + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Remove "info-needed" label + uses: ./actions/python-triage-info-needed + with: + triagers: ${{ env.TRIAGERS }} + action: 'remove' + token: ${{secrets.GITHUB_TOKEN}} diff --git a/.gitignore b/.gitignore index 1ccb649e514f..2fa056f84fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ log.log **/node_modules *.pyc *.vsix +envVars.txt **/.vscode/.ropeproject/** **/testFiles/**/.cache/** *.noseids @@ -22,7 +23,8 @@ cucumber-report.json **/.venv*/ port.txt precommit.hook -pythonFiles/lib/** +python_files/lib/** +python_files/get-pip.py debug_coverage*/** languageServer/** languageServer.*/** @@ -32,7 +34,7 @@ obj/** tmp/** .python-version .vs/ -test-results.xml +test-results*.xml xunit-test-results.xml build/ci/performance/performance-results.json !build/ @@ -41,3 +43,16 @@ debugpy*.log pydevd*.log nodeLanguageServer/** nodeLanguageServer.*/** +dist/** +# translation files +*.xlf +package.nls.*.json +l10n/ +python-env-tools/** +# coverage files produced as test output +python_files/tests/*/.data/.coverage* +python_files/tests/*/.data/*/.coverage* +src/testTestingRootWkspc/coverageWorkspace/.coverage + +# ignore ai artifacts generated and placed in this folder +ai-artifacts/* diff --git a/.nvmrc b/.nvmrc index 764b945d130f..c6a66a6e6a68 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v12.15.0 +v22.21.1 diff --git a/.prettierrc.js b/.prettierrc.js index e584661edd38..87a94b7bf466 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -3,19 +3,13 @@ module.exports = { printWidth: 120, tabWidth: 4, endOfLine: 'auto', - trailingComma: 'none', + trailingComma: 'all', overrides: [ { files: ['*.yml', '*.yaml'], options: { tabWidth: 2 } - }, - { - files: ['**/datascience/serviceRegistry.ts'], - options: { - printWidth: 240 - } } ] }; diff --git a/.sonarcloud.properties b/.sonarcloud.properties index cee889d8051b..9e466689a90a 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,5 +1,4 @@ -sonar.sources=src/client,src/datascience-ui -sonar.exclusions=src/datascience-ui/**/codicon*.* +sonar.sources=src/client 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 +sonar.cpd.exclusions=src/client/activation/**/*.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 508eae4c5ddc..15e6aada1d50 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,8 +2,11 @@ // See https://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ - "ms-vscode.vscode-typescript-tslint-plugin", + "charliermarsh.ruff", "editorconfig.editorconfig", - "esbenp.prettier-vscode" + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-python.python", + "ms-python.vscode-pylance" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 233420f4e6e6..1e983413c8d4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,20 +7,12 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "stopOnEntry": false, + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "smartStep": true, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*", - "!${workspaceFolder}/**/node_modules**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "env": { // Enable this to turn on redux logging during debugging "XVSC_PYTHON_FORCE_LOGGING": "1", @@ -32,64 +24,17 @@ "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, + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data"], "smartStep": true, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*", - "!${workspaceFolder}/**/node_modules**/*" - ], + "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", @@ -101,20 +46,14 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, "sourceMaps": true, "smartStep": true, - "outFiles": [ - "${workspaceFolder}/out/**/*", - "!${workspaceFolder}/**/node_modules**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", "env": { "IS_CI_SERVER_TEST_DEBUGGER": "1" }, - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { // Note, for the smoke test you want to debug, you may need to copy the file, @@ -130,18 +69,14 @@ "--extensionTestsPath=${workspaceFolder}/out/test" ], "env": { - "VSC_PYTHON_CI_TEST_GREP": "Smoke Test" + "VSC_PYTHON_CI_TEST_GREP": "Smoke Test", + "VSC_PYTHON_SMOKE_TEST": "1", + "TEST_FILES_SUFFIX": "smoke.test" }, - "stopOnEntry": false, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "!${workspaceFolder}/**/node_modules**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "Tests (Single Workspace, VS Code, *.test.ts)", @@ -157,46 +92,29 @@ "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**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { - "name": "Tests (DataScience, *.ds.test.ts)", + "name": "Jedi LSP tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "${workspaceFolder}/src/test/datascience", + "${workspaceFolder}/src/test", "--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" + "VSC_PYTHON_CI_TEST_GREP": "Language Server:" }, - "stopOnEntry": false, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "!${workspaceFolder}/**/node_modules**/*" - ], - "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ] + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "preTestJediLSP", + "skipFiles": ["/**"] }, { "name": "Tests (Multiroot, VS Code, *.test.ts)", @@ -209,17 +127,14 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, + "env": { + "VSC_PYTHON_CI_TEST_GREP": "" // Modify this to run a subset of the single workspace tests + }, "sourceMaps": true, "smartStep": true, - "outFiles": [ - "${workspaceFolder}/out/**/*", - "!${workspaceFolder}/**/node_modules**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "Unit Tests (without VS Code, *.unit.test.ts)", @@ -237,14 +152,9 @@ //"--grep", "", "--timeout=300000" ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "!${workspaceFolder}/**/node_modules**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "Unit Tests (fast, without VS Code and without react/monaco, *.unit.test.ts)", @@ -263,14 +173,9 @@ "--timeout=300000", "--fast" ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "!${workspaceFolder}/**/node_modules**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "Functional Tests (without VS Code, *.functional.test.ts)", @@ -299,64 +204,59 @@ // 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" + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output", + // Remove 'X' prefix to dump webview redux action log + "XVSC_PYTHON_WEBVIEW_LOG_FILE": "${workspaceRoot}/test-webview.log" }, - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "!${workspaceFolder}/**/node_modules**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "skipFiles": [ - "/**" - ] + "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": [ - "/**" - ] + "name": "Gulp tasks (helpful for debugging gulpfile.js)", + "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js", + "args": ["watch"], + "skipFiles": ["/**"] }, { - "type": "node", + "name": "Node: Current File", + "program": "${file}", "request": "launch", - "name": "Gulp tasks (helpful for debugging gulpfile.js)", - "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js", - "args": [ - "watch" - ], - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"], + "type": "node" + }, + { + "name": "Python: Current File", + "type": "debugpy", + "justMyCode": true, + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}" + }, + { + "name": "Python: Attach Listen", + "type": "debugpy", + "request": "attach", + "listen": { "host": "localhost", "port": 5678 }, + "justMyCode": true + }, + { + "name": "Debug pytest plugin tests", + + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["${workspaceFolder}/python_files/tests/pytestadapter"], + "justMyCode": true + } + ], + "compounds": [ + { + "name": "Debug Python and Extension", + "configurations": ["Python: Attach Listen", "Extension"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index b2dc13efe26e..01de0d907706 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ { "files.exclude": { "out": true, // set this to true to hide the "out" folder with the compiled JS files + "dist": true, "**/*.pyc": true, ".nyc_output": true, "obj": true, @@ -10,11 +11,11 @@ "**/node_modules": true, ".vscode-test": false, ".vscode test": false, - "**/.mypy_cache/**": true, - "**/.ropeproject/**": true + "**/.mypy_cache/**": true }, "search.exclude": { "out": true, // set this to false to include "out" folder in search results + "dist": true, "**/node_modules": true, "coverage": true, "languageServer*/**": true, @@ -22,39 +23,56 @@ ".vscode test": true }, "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports.isort": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff", + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", "editor.formatOnSave": true }, "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[JSON]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[YAML]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", "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" - ], + "source.fixAll.eslint": "explicit" + }, + "python.languageServer": "Default", "typescript.preferences.importModuleSpecifier": "relative", - "debug.javascript.usePreview": false + // Branch name suggestion. + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", + "git.branchRandomName.enable": true, + "git.branchProtection": ["main", "release/*"], + "git.pullBeforeCheckout": true, + // Open merge editor for resolving conflicts. + "git.mergeEditor": true, + "python.testing.pytestArgs": [ + "python_files/tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "rust-analyzer.linkedProjects": [ + ".\\python-env-tools\\Cargo.toml" + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6f0b07bc7214..c5a054ed43cf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,32 +12,12 @@ "type": "npm", "script": "compile", "isBackground": true, - "problemMatcher": [ - "$tsc-watch", - { - "base": "$tslint5", - "fileLocation": "relative" - } - ], + "problemMatcher": ["$tsc-watch"], "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", @@ -48,13 +28,35 @@ } }, { - "label": "Inject DS WebBrowser UI", + "type": "npm", + "script": "preTestJediLSP", + "problemMatcher": [], + "label": "preTestJediLSP" + }, + { + "type": "npm", + "script": "check-python", + "problemMatcher": ["$python"], + "label": "npm: check-python", + "detail": "npm run check-python:ruff && npm run check-python:pyright" + }, + { + "label": "npm: check-python (venv)", "type": "shell", - "command": "node", - "args": [ - "build/debug/replaceWithWebBrowserPanel.js" - ], - "problemMatcher": [] + "command": "bash", + "args": ["-lc", "source .venv/bin/activate && npm run check-python"], + "problemMatcher": [], + "detail": "Activates the repo .venv first (avoids pyenv/shim Python) then runs: npm run check-python", + "windows": { + "command": "pwsh", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + ".\\.venv\\Scripts\\Activate.ps1; npm run check-python" + ] + } } ] } diff --git a/.vscodeignore b/.vscodeignore index d0da6f84ddf9..d636ab48f361 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,55 +1,36 @@ **/*.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 +**/.env *.vsix -.appveyor.yml .editorconfig .env .eslintrc +.eslintignore .gitattributes .gitignore .gitmodules -.huskyrc.json +.git* .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 +noxfile.py -.devcontainer/** +.config/** .github/** .mocha-reporter/** .nvm/** +.nox/** .nyc_output .prettierrc.js .sonarcloud.properties @@ -70,43 +51,39 @@ debug_coverage*/** images/**/*.gif images/**/*.png ipywidgets/** -news/** +i18n/** 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/python_files/** 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 +python_files/**/*.pyc +python_files/lib/**/*.egg-info/** +python_files/lib/jedilsp/bin/** +python_files/lib/python/bin/** +python_files/tests/** scripts/** src/** test/** tmp/** typings/** types/** +**/__pycache__/** +**/.devcontainer/** + +python-env-tools/.gitignore +python-env-tools/bin/.gitignore +python-env-tools/.github/** +python-env-tools/.vscode/** +python-env-tools/crates/** +python-env-tools/target/** +python-env-tools/Cargo.* +python-env-tools/.cargo/** + +python-env-tools/**/*.md +pythonExtensionApi/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 99392e6969c7..56c1f7697ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,4502 @@ # Changelog -## 2020.7.0-rc (8 July 2020) +**Please see https://github.com/microsoft/vscode-python/releases for the latest release notes. The notes below have been kept for historical purposes.** + +## 2022.10.1 (14 July 2022) + +### Code Health + +- Update app insights key by [karthiknadig](https://github.com/karthiknadig) in ([#19463](https://github.com/microsoft/vscode-python/pull/19463)). + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.10.0 (7 July 2022) + +### Enhancements + +- Add `breakpoint` support for `django-html` & `django-txt` by [Lakshmikanth2001](https://github.com/Lakshmikanth2001) in ([#19288](https://github.com/microsoft/vscode-python/pull/19288)). +- Fix `unittest` discovery issue with experimental component by [ksy7588](https://github.com/ksy7588) in ([#19324](https://github.com/microsoft/vscode-python/pull/19324)). +- Trigger refresh when using `Select Interpreter` command if no envs were found previously by [karrtikr](https://github.com/karrtikr) in ([#19361](https://github.com/microsoft/vscode-python/pull/19361)). +- Update `debugpy` to 1.6.2. + +### Bug Fixes + +- Fix variable name for `flake8Path`'s description by [usta](https://github.com/usta) in ([#19313](https://github.com/microsoft/vscode-python/pull/19313)). +- Ensure we dispose objects on deactivate by [karthiknadig](https://github.com/karthiknadig) in ([#19341](https://github.com/microsoft/vscode-python/pull/19341)). +- Ensure we can change interpreters after trusting a workspace by [karrtikr](https://github.com/karrtikr) in ([#19353](https://github.com/microsoft/vscode-python/pull/19353)). +- Fix for `::::` in node id for `pytest` by [karthiknadig](https://github.com/karthiknadig) in ([#19356](https://github.com/microsoft/vscode-python/pull/19356)). +- Ensure we register for interpreter change when moving from untrusted to trusted. by [karthiknadig](https://github.com/karthiknadig) in ([#19351](https://github.com/microsoft/vscode-python/pull/19351)). + +### Code Health + +- Update CI for using GitHub Actions for release notes by [brettcannon](https://github.com/brettcannon) in ([#19273](https://github.com/microsoft/vscode-python/pull/19273)). +- Add missing translations by [paulacamargo25](https://github.com/paulacamargo25) in ([#19305](https://github.com/microsoft/vscode-python/pull/19305)). +- Delete the `news` directory by [brettcannon](https://github.com/brettcannon) in ([#19308](https://github.com/microsoft/vscode-python/pull/19308)). +- Fix interpreter discovery related telemetry by [karrtikr](https://github.com/karrtikr) in ([#19319](https://github.com/microsoft/vscode-python/pull/19319)). +- Simplify and merge async dispose and dispose by [karthiknadig](https://github.com/karthiknadig) in ([#19348](https://github.com/microsoft/vscode-python/pull/19348)). +- Updating required packages by [karthiknadig](https://github.com/karthiknadig) in ([#19375](https://github.com/microsoft/vscode-python/pull/19375)). +- Update the issue notebook by [brettcannon](https://github.com/brettcannon) in ([#19388](https://github.com/microsoft/vscode-python/pull/19388)). +- Remove `notebookeditor` proposed API by [karthiknadig](https://github.com/karthiknadig) in ([#19392](https://github.com/microsoft/vscode-python/pull/19392)). + +**Full Changelog**: https://github.com/microsoft/vscode-python/compare/2022.8.1...2022.10.0 + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.8.1 (28 June 2022) + +### Code Health + +1. Update vscode `extension-telemetry` package. + ([#19375](https://github.com/microsoft/vscode-python/pull/19375)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.8.0 (9 June 2022) + +### Enhancements + +1. Make cursor focus switch automatically to the terminal after launching a python process with configuration option. (Thanks [djplt](https://github.com/djplt)) + ([#14851](https://github.com/Microsoft/vscode-python/issues/14851)) +1. Enable localization using vscode-nls. + ([#18286](https://github.com/Microsoft/vscode-python/issues/18286)) +1. Add support for referencing multiroot-workspace folders in settings using `${workspaceFolder:}`. + ([#18650](https://github.com/Microsoft/vscode-python/issues/18650)) +1. Ensure conda envs lacking an interpreter which do not use a valid python binary are also discovered and is selectable, so that `conda env list` matches with what the extension reports. + ([#18934](https://github.com/Microsoft/vscode-python/issues/18934)) +1. Improve information collected by the `Python: Report Issue` command. + ([#19067](https://github.com/Microsoft/vscode-python/issues/19067)) +1. Only trigger auto environment discovery if a user attempts to choose a different interpreter, or when a particular scope (a workspace folder or globally) is opened for the first time. + ([#19102](https://github.com/Microsoft/vscode-python/issues/19102)) +1. Added a proposed API to report progress of environment discovery in two phases. + ([#19103](https://github.com/Microsoft/vscode-python/issues/19103)) +1. Update to latest LS client (v8.0.0) and server (v8.0.0). + ([#19114](https://github.com/Microsoft/vscode-python/issues/19114)) +1. Update to latest LS client (v8.0.1) and server (v8.0.1) that contain the race condition fix around `LangClient.stop`. + ([#19139](https://github.com/Microsoft/vscode-python/issues/19139)) + +### Fixes + +1. Do not use `--user` flag when installing in a virtual environment. + ([#14327](https://github.com/Microsoft/vscode-python/issues/14327)) +1. Fix error `No such file or directory` on conda activate, and simplify the environment activation code. + ([#18989](https://github.com/Microsoft/vscode-python/issues/18989)) +1. Add proposed async execution API under environments. + ([#19079](https://github.com/Microsoft/vscode-python/issues/19079)) + +### Code Health + +1. Capture whether environment discovery was triggered using Quickpick UI. + ([#19077](https://github.com/Microsoft/vscode-python/issues/19077)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.6.0 (5 May 2022) + +### Enhancements + +1. Rewrite support for unittest test discovery. + ([#17242](https://github.com/Microsoft/vscode-python/issues/17242)) +1. Do not require a reload when swapping between language servers. + ([#18509](https://github.com/Microsoft/vscode-python/issues/18509)) + +### Fixes + +1. Do not show inherit env prompt for conda envs when running "remotely". + ([#18510](https://github.com/Microsoft/vscode-python/issues/18510)) +1. Fixes invalid regular expression logging error occurs when file paths contain special characters. + (Thanks [sunyinqi0508](https://github.com/sunyinqi0508)) + ([#18829](https://github.com/Microsoft/vscode-python/issues/18829)) +1. Do not prompt to select new virtual envrionment if it has already been selected. + ([#18915](https://github.com/Microsoft/vscode-python/issues/18915)) +1. Disable isort when using isort extension. + ([#18945](https://github.com/Microsoft/vscode-python/issues/18945)) +1. Remove `process` check from browser specific entry point for the extension. + ([#18974](https://github.com/Microsoft/vscode-python/issues/18974)) +1. Use built-in test refresh button. + ([#19012](https://github.com/Microsoft/vscode-python/issues/19012)) +1. Update vscode-telemetry-extractor to @vscode/telemetry-extractor@1.9.7. + (Thanks [Quan Zhuo](https://github.com/quanzhuo)) + ([#19036](https://github.com/Microsoft/vscode-python/issues/19036)) +1. Ensure 64-bit interpreters are preferred over 32-bit when auto-selecting. + ([#19042](https://github.com/Microsoft/vscode-python/issues/19042)) + +### Code Health + +1. Update Jedi minimum to python 3.7. + ([#18324](https://github.com/Microsoft/vscode-python/issues/18324)) +1. Stop using `--live-stream` when using `conda run` (see https://github.com/conda/conda/issues/11209 for details). + ([#18511](https://github.com/Microsoft/vscode-python/issues/18511)) +1. Remove prompt to recommend users in old insiders program to switch to pre-release. + ([#18809](https://github.com/Microsoft/vscode-python/issues/18809)) +1. Update requirements to remove python 2.7 version restrictions. + ([#19060](https://github.com/Microsoft/vscode-python/issues/19060)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.4.1 (7 April 2022) + +### Fixes + +1. Ensure `conda info` command isn't run multiple times during startup when large number of conda interpreters are present. + ([#18200](https://github.com/Microsoft/vscode-python/issues/18200)) +1. If a conda environment is not returned via the `conda env list` command, consider it as unknown env type. + ([#18530](https://github.com/Microsoft/vscode-python/issues/18530)) +1. Wrap file paths containing an ampersand in double quotation marks for running commands in a shell. + ([#18722](https://github.com/Microsoft/vscode-python/issues/18722)) +1. Fixes regression with support for python binaries not following the standard names. + ([#18835](https://github.com/Microsoft/vscode-python/issues/18835)) +1. Fix launch of Python Debugger when using conda environments. + ([#18847](https://github.com/Microsoft/vscode-python/issues/18847)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.4.0 (30 March 2022) + +### Enhancements + +1. Use new pre-release mechanism to install insiders. + ([#18144](https://github.com/Microsoft/vscode-python/issues/18144)) +1. Add support for detection and selection of conda environments lacking a python interpreter. + ([#18357](https://github.com/Microsoft/vscode-python/issues/18357)) +1. Retains the state of the TensorBoard webview. + ([#18591](https://github.com/Microsoft/vscode-python/issues/18591)) +1. Move interpreter info status bar item to the right. + ([#18710](https://github.com/Microsoft/vscode-python/issues/18710)) +1. `debugpy` updated to version `v1.6.0`. + ([#18795](https://github.com/Microsoft/vscode-python/issues/18795)) + +### Fixes + +1. Properly dismiss the error popup dialog when having a linter error. (Thanks [Virgil Sisoe](https://github.com/sisoe24)) + ([#18553](https://github.com/Microsoft/vscode-python/issues/18553)) +1. Python files are no longer excluded from Pytest arguments during test discovery. + (thanks [Marc Mueller](https://github.com/cdce8p/)) + ([#18562](https://github.com/Microsoft/vscode-python/issues/18562)) +1. Fixes regression caused due to using `conda run` for executing files. + ([#18634](https://github.com/Microsoft/vscode-python/issues/18634)) +1. Use `conda run` to get the activated environment variables instead of activation using shell scripts. + ([#18698](https://github.com/Microsoft/vscode-python/issues/18698)) + +### Code Health + +1. Remove old settings migrator. + ([#14334](https://github.com/Microsoft/vscode-python/issues/14334)) +1. Remove old language server setting migration. + ([#14337](https://github.com/Microsoft/vscode-python/issues/14337)) +1. Remove dependency on other file system watchers. + ([#18381](https://github.com/Microsoft/vscode-python/issues/18381)) +1. Update TypeScript version to 4.5.5. + ([#18602](https://github.com/Microsoft/vscode-python/issues/18602)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.2.0 (3 March 2022) + +### Enhancements + +1. Implement a "New Python File" command + ([#18376](https://github.com/Microsoft/vscode-python/issues/18376)) +1. Use `conda run` for conda environments for running python files and installing modules. + ([#18479](https://github.com/Microsoft/vscode-python/issues/18479)) +1. Better filename patterns for pip-requirements. + (thanks [Baptiste Darthenay](https://github.com/batisteo)) + ([#18498](https://github.com/Microsoft/vscode-python/issues/18498)) + +### Fixes + +1. Ensure clicking "Discovering Python Interpreters" in the status bar shows the current discovery progress. + ([#18443](https://github.com/Microsoft/vscode-python/issues/18443)) +1. Fixes Pylama output parsing with MyPy. (thanks [Nicola Marella](https://github.com/nicolamarella)) + ([#15609](https://github.com/Microsoft/vscode-python/issues/15609)) +1. Fix CPU load issue caused by poetry plugin by not watching directories which do not exist. + ([#18459](https://github.com/Microsoft/vscode-python/issues/18459)) +1. Explicitly add `"justMyCode": "true"` to all `launch.json` configurations. + (Thanks [Matt Bogosian](https://github.com/posita)) + ([#18471](https://github.com/Microsoft/vscode-python/issues/18471)) +1. Identify base conda environments inside pyenv correctly. + ([#18500](https://github.com/Microsoft/vscode-python/issues/18500)) +1. Fix for a crash when loading environments with no info. + ([#18594](https://github.com/Microsoft/vscode-python/issues/18594)) + +### Code Health + +1. Remove dependency on `ts-mock-imports`. + ([#14757](https://github.com/Microsoft/vscode-python/issues/14757)) +1. Update `vsce` to `v2.6.6`. + ([#18411](https://github.com/Microsoft/vscode-python/issues/18411)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.0.1 (8 February 2022) + +### Fixes + +1. Fix `invalid patch string` error when using conda. + ([#18455](https://github.com/Microsoft/vscode-python/issues/18455)) +1. Revert to old way of running debugger if conda version less than 4.9.0. + ([#18436](https://github.com/Microsoft/vscode-python/issues/18436)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2022.0.0 (3 February 2022) + +### Enhancements + +1. Add support for conda run without output, using `--no-capture-output` flag. + ([#7696](https://github.com/Microsoft/vscode-python/issues/7696)) +1. Add an option to clear interpreter setting for all workspace folders in multiroot scenario. + ([#17693](https://github.com/Microsoft/vscode-python/issues/17693)) +1. Public API for environments (proposed). + ([#17905](https://github.com/Microsoft/vscode-python/issues/17905)) +1. Group interpreters in interpreter quick picker using separators. + ([#17944](https://github.com/Microsoft/vscode-python/issues/17944)) +1. Add support for pylint error ranges. Requires Python 3.8 and pylint 2.12.2 or higher. (thanks [Marc Mueller](https://github.com/cdce8p)) + ([#18068](https://github.com/Microsoft/vscode-python/issues/18068)) +1. Move pinned interpreter status bar item towards the right behind `pythonInterpreterInfoPinned` experiment. + ([#18282](https://github.com/Microsoft/vscode-python/issues/18282)) +1. Move interpreter status bar item into the `Python` language status item behind `pythonInterpreterInfoUnpinned` experiment. + ([#18283](https://github.com/Microsoft/vscode-python/issues/18283)) +1. Update Jedi language server to latest. + ([#18325](https://github.com/Microsoft/vscode-python/issues/18325)) + +### Fixes + +1. Update zh-tw translations. (thanks [ted1030](https://github.com/ted1030)) + ([#17991](https://github.com/Microsoft/vscode-python/issues/17991)) +1. Support selecting conda environments with python `3.10`. + ([#18128](https://github.com/Microsoft/vscode-python/issues/18128)) +1. Fixes to telemetry handler in language server middleware. + ([#18188](https://github.com/Microsoft/vscode-python/issues/18188)) +1. Resolve system variables in `python.defaultInterpreterPath`. + ([#18207](https://github.com/Microsoft/vscode-python/issues/18207)) +1. Ensures interpreters are discovered even when running `interpreterInfo.py` script prints more than just the script output. + ([#18234](https://github.com/Microsoft/vscode-python/issues/18234)) +1. Remove restrictions on using `purpose` in debug configuration. + ([#18248](https://github.com/Microsoft/vscode-python/issues/18248)) +1. Ensure Python Interpreter information in the status bar is updated if Interpreter information changes. + ([#18257](https://github.com/Microsoft/vscode-python/issues/18257)) +1. Fix "Run Selection/Line in Python Terminal" for Python < 3.8 when the code includes decorators. + ([#18258](https://github.com/Microsoft/vscode-python/issues/18258)) +1. Ignore notebook cells for pylance. Jupyter extension is handling notebooks. + ([#18259](https://github.com/Microsoft/vscode-python/issues/18259)) +1. Fix for UriError when using python.interpreterPath command in tasks. + ([#18285](https://github.com/Microsoft/vscode-python/issues/18285)) +1. Ensure linting works under `conda run` (work-around for https://github.com/conda/conda/issues/10972). + ([#18364](https://github.com/Microsoft/vscode-python/issues/18364)) +1. Ensure items are removed from the array in reverse order when using array indices. + ([#18382](https://github.com/Microsoft/vscode-python/issues/18382)) +1. Log experiments only after we finish updating active experiments list. + ([#18393](https://github.com/Microsoft/vscode-python/issues/18393)) + +### Code Health + +1. Improve unit tests for envVarsService, in particular the variable substitution logic (Thanks [Keshav Kini](https://github.com/kini)) + ([#17747](https://github.com/Microsoft/vscode-python/issues/17747)) +1. Remove `python.pythonPath` setting and `pythonDeprecatePythonPath` experiment. + ([#17977](https://github.com/Microsoft/vscode-python/issues/17977)) +1. Remove `pythonTensorboardExperiment` and `PythonPyTorchProfiler` experiments. + ([#18074](https://github.com/Microsoft/vscode-python/issues/18074)) +1. Reduce direct dependency on IOutputChannel. + ([#18132](https://github.com/Microsoft/vscode-python/issues/18132)) +1. Upgrade to Node 14 LTS (v14.18.2). + ([#18148](https://github.com/Microsoft/vscode-python/issues/18148)) +1. Switch `jedils_requirements.txt` to `requirements.txt` under `pythonFiles/jedilsp_requirements/`. + ([#18185](https://github.com/Microsoft/vscode-python/issues/18185)) +1. Removed `experiments.json` file. + ([#18235](https://github.com/Microsoft/vscode-python/issues/18235)) +1. Fixed typescript and namespace errors. (Thanks [Harry-Hopkinson](https://github.com/Harry-Hopkinson)) + ([#18345](https://github.com/Microsoft/vscode-python/issues/18345)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Pylance](https://github.com/microsoft/pylance-release) + +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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.12.0 (9 December 2021) + +### Enhancements + +1. Python extension should activate on onDebugInitialConfigurations. + (thanks [Nayana Vinod](https://github.com/nayana-vinod) and [Jessica Jolly](https://github.com/JessieJolly)). + ([#9557](https://github.com/Microsoft/vscode-python/issues/9557)) +1. Declare limited support when running in virtual workspaces by only supporting language servers. + ([#17519](https://github.com/Microsoft/vscode-python/issues/17519)) +1. Add a "Do not show again" option to the formatter installation prompt. + ([#17937](https://github.com/Microsoft/vscode-python/issues/17937)) +1. Add the ability to install `pip` if missing, when installing missing packages from the `Jupyter Extension`. + ([#17975](https://github.com/Microsoft/vscode-python/issues/17975)) +1. Declare limited support for untrusted workspaces by only supporting Pylance. + ([#18031](https://github.com/Microsoft/vscode-python/issues/18031)) +1. Update to latest jedi language server. + ([#18051](https://github.com/Microsoft/vscode-python/issues/18051)) +1. Add language status item indicating that extension works partially in virtual and untrusted workspaces. + ([#18059](https://github.com/Microsoft/vscode-python/issues/18059)) + +### Fixes + +1. Partial fix for using the same directory as discovery when running tests. + (thanks [Brian Rutledge](https://github.com/bhrutledge)) + ([#9553](https://github.com/Microsoft/vscode-python/issues/9553)) +1. Handle decorators properly when using the `Run Selection/Line in Python Terminal` command. + ([#15058](https://github.com/Microsoft/vscode-python/issues/15058)) +1. Don't interpret `--rootdir` as a test folder for `pytest`. + (thanks [Brian Rutledge](https://github.com/bhrutledge)) + ([#16079](https://github.com/Microsoft/vscode-python/issues/16079)) +1. Ensure debug configuration env variables overwrite env variables defined in .env file. + ([#16984](https://github.com/Microsoft/vscode-python/issues/16984)) +1. Fix for `pytest` run all tests when using `pytest.ini` and `cwd`. + (thanks [Brian Rutledge](https://github.com/bhrutledge)) + ([#17546](https://github.com/Microsoft/vscode-python/issues/17546)) +1. When parsing pytest node ids with parameters, use native pytest information to separate out the parameter decoration rather than try and parse the nodeid as text. + (thanks [Martijn Pieters](https://github.com/mjpieters)) + ([#17676](https://github.com/Microsoft/vscode-python/issues/17676)) +1. Do not process system Python 2 installs on macOS Monterey. + ([#17870](https://github.com/Microsoft/vscode-python/issues/17870)) +1. Remove duplicate "Clear Workspace Interpreter Setting" command from the command palette. + ([#17890](https://github.com/Microsoft/vscode-python/issues/17890)) +1. Ensure that path towards extenal tools like linters are not synched between + machines. (thanks [Sorin Sbarnea](https://github.com/ssbarnea)) + ([#18008](https://github.com/Microsoft/vscode-python/issues/18008)) +1. Increase timeout for activation of conda environments from 30s to 60s. + ([#18017](https://github.com/Microsoft/vscode-python/issues/18017)) + +### Code Health + +1. Removing experiments for refresh and failed tests buttons. + ([#17868](https://github.com/Microsoft/vscode-python/issues/17868)) +1. Remove caching debug configuration experiment only. + ([#17895](https://github.com/Microsoft/vscode-python/issues/17895)) +1. Remove "join mailing list" notification experiment. + ([#17904](https://github.com/Microsoft/vscode-python/issues/17904)) +1. Remove dependency on `winston` logger. + ([#17921](https://github.com/Microsoft/vscode-python/issues/17921)) +1. Bump isort from 5.9.3 to 5.10.0. + ([#17923](https://github.com/Microsoft/vscode-python/issues/17923)) +1. Remove old discovery code and discovery experiments. + ([#17962](https://github.com/Microsoft/vscode-python/issues/17962)) +1. Remove dependency on `azure-storage`. + ([#17972](https://github.com/Microsoft/vscode-python/issues/17972)) +1. Ensure telemetry correctly identifies when users set linter paths. + ([#18019](https://github.com/Microsoft/vscode-python/issues/18019)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.11.0 (4 November 2021) + +### Enhancements + +1. Improve setting description for enabling A/B tests. (Thanks [Thi Le](https://github.com/thi-lee)) + ([#7793](https://github.com/Microsoft/vscode-python/issues/7793)) +1. Support `expectedFailure` when running `unittest` tests using `pytest`. + ([#8427](https://github.com/Microsoft/vscode-python/issues/8427)) +1. Support environment variable substitution in `python` property for `launch.json`. + ([#12289](https://github.com/Microsoft/vscode-python/issues/12289)) +1. Update homebrew instructions to install python 3. + (thanks [Carolinekung2 ](https://github.com/Carolinekung2)) + ([#17590](https://github.com/Microsoft/vscode-python/issues/17590)) + +### Fixes + +1. Reworded message for A/B testing in the output channel to "Experiment 'X' is active/inactive". + (Thanks [Vidushi Gupta](https://github.com/Vidushi-Gupta) for the contribution) + ([#6352](https://github.com/Microsoft/vscode-python/issues/6352)) +1. Change text to "Select at workspace level" instead of "Entire workspace" when selecting or clearing interpreters in a multiroot folder scenario. + (Thanks [Quynh Do](https://github.com/quynhd07)) + ([#10737](https://github.com/Microsoft/vscode-python/issues/10737)) +1. Fix unresponsive extension issues caused by discovery component. + ([#11924](https://github.com/Microsoft/vscode-python/issues/11924)) +1. Remove duplicate 'Run Python file' commands in command palette. + ([#14562](https://github.com/Microsoft/vscode-python/issues/14562)) +1. Change drive first before changing directory in windows, to anticipate running file outside working directory with different storage drive. (thanks [afikrim](https://github.com/afikrim)) + ([#14730](https://github.com/Microsoft/vscode-python/issues/14730)) +1. Support installing Insiders extension in remote sessions. + ([#15145](https://github.com/Microsoft/vscode-python/issues/15145)) +1. If the executeInFileDir setting is enabled, always change to the script directory before running the script, even if the script is in the Workspace folder. (thanks (acash715)[https://github.com/acash715]) + ([#15181](https://github.com/Microsoft/vscode-python/issues/15181)) +1. replaceAll for replacing separators. (thanks [Aliva Das](https://github.com/IceJinx33)) + ([#15288](https://github.com/Microsoft/vscode-python/issues/15288)) +1. When activating environment, creating new Integrated Terminal doesn't take selected workspace into account. (Thanks [Vidushi Gupta](https://github.com/Vidushi-Gupta) for the contribution) + ([#15522](https://github.com/Microsoft/vscode-python/issues/15522)) +1. Fix truncated mypy errors by setting `--no-pretty`. + (thanks [Peter Lithammer](https://github.com/lithammer)) + ([#16836](https://github.com/Microsoft/vscode-python/issues/16836)) +1. Renamed the commands in the Run/Debug button of the editor title. (thanks (Analía Bannura)[https://github.com/analiabs] and (Anna Arsentieva)[https://github.com/arsentieva]) + ([#17019](https://github.com/Microsoft/vscode-python/issues/17019)) +1. Fix for `pytest` run all tests when using `pytest.ini`. + ([#17546](https://github.com/Microsoft/vscode-python/issues/17546)) +1. Ensures test node is updated when `unittest` sub-tests are used. + ([#17561](https://github.com/Microsoft/vscode-python/issues/17561)) +1. Update debugpy to 1.5.1 to ensure user-unhandled exception setting is false by default. + ([#17789](https://github.com/Microsoft/vscode-python/issues/17789)) +1. Ensure we filter out unsupported features in web scenario using `shellExecutionSupported` context key. + ([#17811](https://github.com/Microsoft/vscode-python/issues/17811)) +1. Remove `python.condaPath` from workspace scope. + ([#17819](https://github.com/Microsoft/vscode-python/issues/17819)) +1. Make updateTestItemFromRawData async to prevent blocking the extension. + ([#17823](https://github.com/Microsoft/vscode-python/issues/17823)) +1. Semantic colorization can sometimes require reopening or scrolling of a file. + ([#17878](https://github.com/Microsoft/vscode-python/issues/17878)) + +### Code Health + +1. Remove TSLint comments since we use ESLint. + ([#4060](https://github.com/Microsoft/vscode-python/issues/4060)) +1. Remove unused SHA512 hashing code. + ([#7333](https://github.com/Microsoft/vscode-python/issues/7333)) +1. Remove unused packages. + ([#16840](https://github.com/Microsoft/vscode-python/issues/16840)) +1. Remove old discovery code and discovery experiments. + ([#17795](https://github.com/Microsoft/vscode-python/issues/17795)) +1. Do not query for version and kind if it's not needed when reporting an issue. + ([#17815](https://github.com/Microsoft/vscode-python/issues/17815)) +1. Remove Microsoft Python Language Server support from the extension. + ([#17834](https://github.com/Microsoft/vscode-python/issues/17834)) +1. Bump `packaging` from 21.0 to 21.2. + ([#17886](https://github.com/Microsoft/vscode-python/issues/17886)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.10.1 (13 October 2021) + +### Enhancements + +1. Provide IntelliSense status information when using `github.dev` or any other web platform. + ([#17658](https://github.com/Microsoft/vscode-python/issues/17658)) + +### Fixes + +1. Ensure commands run are not logged twice in Python output channel. + ([#7160](https://github.com/Microsoft/vscode-python/issues/7160)) +1. Ensure we use fragment when formatting notebook cells. + ([#16980](https://github.com/Microsoft/vscode-python/issues/16980)) +1. Hide UI elements that are not applicable when using `github.dev` or any other web platform. + ([#17252](https://github.com/Microsoft/vscode-python/issues/17252)) +1. Localize strings on `github.dev` using VSCode FS API. + ([#17712](https://github.com/Microsoft/vscode-python/issues/17712)) + +### Code Health + +1. Log commands run by the discovery component in the output channel. + ([#16732](https://github.com/Microsoft/vscode-python/issues/16732)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.10.0 (7 October 2021) + +### Enhancements + +1. Set the default value of `python.linting.pylintEnabled` to `false`. + ([#3007](https://github.com/Microsoft/vscode-python/issues/3007)) +1. Phase out Jedi 0.17, and use Jedi behind a language server protocol as the Jedi option. Remove Jedi-related settings `python.jediMemoryLimit` and `python.jediPath`, since they are not used with the new language server implementation. + ([#11995](https://github.com/Microsoft/vscode-python/issues/11995)) +1. Add support for dynamic updates in interpreter list. + ([#17043](https://github.com/Microsoft/vscode-python/issues/17043)) +1. Query for fresh workspace envs when auto-selecting interpreters in a new workspace. + ([#17264](https://github.com/Microsoft/vscode-python/issues/17264)) +1. Increase Microsoft Python Language Server deprecation prompt frequency and update wording. + ([#17361](https://github.com/Microsoft/vscode-python/issues/17361)) +1. Remove "The Python extension will have limited support for Python 2.7 in the next release" notification. + ([#17451](https://github.com/Microsoft/vscode-python/issues/17451)) +1. Added non-blocking discovery APIs for Jupyter. + ([#17452](https://github.com/Microsoft/vscode-python/issues/17452)) +1. Resolve environments using cache if cache has complete env info. + ([#17474](https://github.com/Microsoft/vscode-python/issues/17474)) +1. Ensure debugger contribution points are turned off when using virtual workspaces. + ([#17493](https://github.com/Microsoft/vscode-python/issues/17493)) +1. Display a notification about the end of Jedi support when using Python 2.7. + ([#17512](https://github.com/Microsoft/vscode-python/issues/17512)) +1. If user has selected an interpreter which is not discovery cache, correctly add it to cache. + ([#17575](https://github.com/Microsoft/vscode-python/issues/17575)) +1. Update to latest version of Jedi LS. + ([#17591](https://github.com/Microsoft/vscode-python/issues/17591)) +1. Update to `vscode-extension-telemetry` 0.4.2. + ([#17608](https://github.com/Microsoft/vscode-python/issues/17608)) + +### Fixes + +1. Don't override user provided `--rootdir` in pytest args. + ([#8678](https://github.com/Microsoft/vscode-python/issues/8678)) +1. Don't log error during settings migration if settings.json doesn't exist. + ([#11354](https://github.com/Microsoft/vscode-python/issues/11354)) +1. Fix casing of text in `unittest` patterns quickpick. + (thanks [Anupama Nadig](https://github.com/anu-ka)) + ([#17093](https://github.com/Microsoft/vscode-python/issues/17093)) +1. Use quickpick details for the "Use Python from `python.defaultInterpreterPath` setting" entry. + ([#17124](https://github.com/Microsoft/vscode-python/issues/17124)) +1. Fix refreshing progress display in the status bar. + ([#17338](https://github.com/Microsoft/vscode-python/issues/17338)) +1. Ensure we do not start a new discovery for an event if one is already scheduled. + ([#17339](https://github.com/Microsoft/vscode-python/issues/17339)) +1. Do not display workspace related envs if no workspace is open. + ([#17358](https://github.com/Microsoft/vscode-python/issues/17358)) +1. Ensure we correctly evaluate Unknown type before sending startup telemetry. + ([#17362](https://github.com/Microsoft/vscode-python/issues/17362)) +1. Fix for unittest discovery failure due to root id mismatch. + ([#17386](https://github.com/Microsoft/vscode-python/issues/17386)) +1. Improve pattern matching for shell detection on Windows. + (thanks [Erik Demaine](https://github.com/edemaine/)) + ([#17426](https://github.com/Microsoft/vscode-python/issues/17426)) +1. Changed the way of searching left bracket `[` in case of subsets of tests. + (thanks [ilexei](https://github.com/ilexei)) + ([#17461](https://github.com/Microsoft/vscode-python/issues/17461)) +1. Fix hang caused by loop in getting interpreter information. + ([#17484](https://github.com/Microsoft/vscode-python/issues/17484)) +1. Ensure database storage extension uses to track all storages does not grow unnecessarily. + ([#17488](https://github.com/Microsoft/vscode-python/issues/17488)) +1. Ensure all users use new discovery code regardless of their experiment settings. + ([#17563](https://github.com/Microsoft/vscode-python/issues/17563)) +1. Add timeout when discovery runs `conda info --json` command. + ([#17576](https://github.com/Microsoft/vscode-python/issues/17576)) +1. Use `conda-forge` channel when installing packages into conda environments. + ([#17628](https://github.com/Microsoft/vscode-python/issues/17628)) + +### Code Health + +1. Remove support for `rope`. Refactoring now supported via language servers. + ([#10440](https://github.com/Microsoft/vscode-python/issues/10440)) +1. Remove `pylintMinimalCheckers` setting. Syntax errors now reported via language servers. + ([#13321](https://github.com/Microsoft/vscode-python/issues/13321)) +1. Remove `ctags` support. Workspace symbols now supported via language servers. + ([#16063](https://github.com/Microsoft/vscode-python/issues/16063)) +1. Fix linting for some files in .eslintignore. + ([#17181](https://github.com/Microsoft/vscode-python/issues/17181)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.9.3 (20 September 2021) + +### Fixes + +1. Fix `Python extension loading...` issue for users who have disabled telemetry. + ([#17447](https://github.com/Microsoft/vscode-python/issues/17447)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.9.2 (13 September 2021) + +### Fixes + +1. Ensure line feeds are changed to CRLF in test messages. + ([#17111](https://github.com/Microsoft/vscode-python/issues/17111)) +1. Fix for `unittest` ModuleNotFoundError when discovering tests. + ([#17363](https://github.com/Microsoft/vscode-python/issues/17363)) +1. Ensure we block getting active interpreter on auto-selection. + ([#17370](https://github.com/Microsoft/vscode-python/issues/17370)) +1. Fix to handle undefined uri in debug in terminal command. + ([#17374](https://github.com/Microsoft/vscode-python/issues/17374)) +1. Fix for missing buttons for tests when using multiple test folders. + ([#17378](https://github.com/Microsoft/vscode-python/issues/17378)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.9.1 (9 September 2021) + +### Fixes + +1. Fix for debug configuration used when no launch.json exists is still used after launch.json is created. + ([#17353](https://github.com/Microsoft/vscode-python/issues/17353)) +1. Ensure default python executable to use is 'python' instead of ''. + ([#17089](https://github.com/Microsoft/vscode-python/issues/17089)) +1. Ensure workspace interpreters are discovered and watched when in `pythonDiscoveryModuleWithoutWatcher` experiment. + ([#17144](https://github.com/Microsoft/vscode-python/issues/17144)) +1. Do path comparisons appropriately in the new discovery component. + ([#17244](https://github.com/Microsoft/vscode-python/issues/17244)) +1. Fix for test result not found for files starting with py. + ([#17270](https://github.com/Microsoft/vscode-python/issues/17270)) +1. Fix for unable to import when running unittest. + ([#17280](https://github.com/Microsoft/vscode-python/issues/17280)) +1. Fix for multiple folders in `pytest` args. + ([#17281](https://github.com/Microsoft/vscode-python/issues/17281)) +1. Fix issue with incomplete `unittest` runs. + ([#17282](https://github.com/Microsoft/vscode-python/issues/17282)) +1. Improve detecting lines when using testing wrappers. + ([#17285](https://github.com/Microsoft/vscode-python/issues/17285)) +1. Ensure we trigger discovery for the first time as part of extension activation. + ([#17303](https://github.com/Microsoft/vscode-python/issues/17303)) +1. Correctly indicate when interpreter refresh has finished. + ([#17335](https://github.com/Microsoft/vscode-python/issues/17335)) +1. Missing location info for `async def` functions. + ([#17309](https://github.com/Microsoft/vscode-python/issues/17309)) +1. For CI ensure `tensorboard` is installed in python 3 environments only. + ([#17325](https://github.com/Microsoft/vscode-python/issues/17325)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.9.0 (1 September 2021) + +### Enhancements + +1. Added commands to select and run a set of tests. + ([#3652](https://github.com/Microsoft/vscode-python/issues/3652)) +1. Fix for tests should be re-discovered after switching environment. + ([#5347](https://github.com/Microsoft/vscode-python/issues/5347)) +1. Remove the testing functionality from the status bar. + ([#8405](https://github.com/Microsoft/vscode-python/issues/8405)) +1. Automatically detect new test file in test explorer. + ([#8675](https://github.com/Microsoft/vscode-python/issues/8675)) +1. Search test names in test explorer. + ([#8836](https://github.com/Microsoft/vscode-python/issues/8836)) +1. Added a command for displaying the test explorer. + ([#9026](https://github.com/Microsoft/vscode-python/issues/9026)) +1. Make "run all tests" icon gray instead of green. + ([#9402](https://github.com/Microsoft/vscode-python/issues/9402)) +1. Use VS Code's test UI instead of code lenses above tests. + ([#10898](https://github.com/Microsoft/vscode-python/issues/10898)) +1. Added command to run last executed test. + ([#11864](https://github.com/Microsoft/vscode-python/issues/11864)) +1. Fix for PyTest discovery can fail but not give any clue as to what the problem is. + ([#12043](https://github.com/Microsoft/vscode-python/issues/12043)) +1. Add shortcut to run the current test (at cursor position). + ([#12218](https://github.com/Microsoft/vscode-python/issues/12218)) +1. Run all tests in a multi-root workspace without prompting. + ([#13147](https://github.com/Microsoft/vscode-python/issues/13147)) +1. Plug into VS Code's Test UI. + ([#15750](https://github.com/Microsoft/vscode-python/issues/15750)) +1. Show notification to join insiders after 5 mins. + ([#16833](https://github.com/Microsoft/vscode-python/issues/16833)) +1. Update Simplified Chinese translation. (thanks [FiftysixTimes7](https://github.com/FiftysixTimes7)) + ([#16916](https://github.com/Microsoft/vscode-python/issues/16916)) +1. Added Debug file button to editor run menu. + ([#16924](https://github.com/Microsoft/vscode-python/issues/16924)) +1. Cache last selection for debug configuration when debugging without launch.json. + ([#16934](https://github.com/Microsoft/vscode-python/issues/16934)) +1. Improve display of default interpreter and suggested interpreter in the interpreter selection quick pick. + ([#16971](https://github.com/Microsoft/vscode-python/issues/16971)) +1. Improve discovery component API. + ([#17005](https://github.com/Microsoft/vscode-python/issues/17005)) +1. Add a notification about Python 2.7 support, displayed whenever a tool is used or whenever debugging is started. + ([#17009](https://github.com/Microsoft/vscode-python/issues/17009)) +1. Add caching debug configuration behind experiment. + ([#17025](https://github.com/Microsoft/vscode-python/issues/17025)) +1. Do not query to get all interpreters where it's not needed in the extension code. + ([#17030](https://github.com/Microsoft/vscode-python/issues/17030)) +1. Add a warning prompt for the Microsoft Python Language Server deprecation. + ([#17056](https://github.com/Microsoft/vscode-python/issues/17056)) +1. Update to latest jedi-language-server. + ([#17072](https://github.com/Microsoft/vscode-python/issues/17072)) + +### Fixes + +1. Fix for test code lenses do not disappear even after disabling the unit tests. + ([#1654](https://github.com/Microsoft/vscode-python/issues/1654)) +1. Fix for code lens for a test class run under unittest doesn't show overall results for methods. + ([#2382](https://github.com/Microsoft/vscode-python/issues/2382)) +1. Fix for test code lens do not appear on initial activation of testing support. + ([#2644](https://github.com/Microsoft/vscode-python/issues/2644)) +1. Fix for "No tests ran, please check the configuration settings for the tests". + ([#2660](https://github.com/Microsoft/vscode-python/issues/2660)) +1. Fix for code lenses disappear on save, then re-appear when tabbing on/off the file. + ([#2790](https://github.com/Microsoft/vscode-python/issues/2790)) +1. Fix for code lenses for tests not showing up when test is defined on line 1. + ([#3062](https://github.com/Microsoft/vscode-python/issues/3062)) +1. Fix for command 'python.runtests' not found. + ([#3591](https://github.com/Microsoft/vscode-python/issues/3591)) +1. Fix for navigation to code doesn't work with parameterized tests. + ([#4469](https://github.com/Microsoft/vscode-python/issues/4469)) +1. Fix for tests are not being discovered at first in multiroot workspace. + ([#4848](https://github.com/Microsoft/vscode-python/issues/4848)) +1. Fix for tests not found after upgrade. + ([#5417](https://github.com/Microsoft/vscode-python/issues/5417)) +1. Fix for failed icon of the first failed test doesn't changed to running icon when using unittest framework. + ([#5791](https://github.com/Microsoft/vscode-python/issues/5791)) +1. Fix for failure details in unittest discovery are not always logged. + ([#5889](https://github.com/Microsoft/vscode-python/issues/5889)) +1. Fix for test results not updated if test is run via codelens. + ([#6787](https://github.com/Microsoft/vscode-python/issues/6787)) +1. Fix for "Run Current Test File" is not running tests, just discovering them. + ([#7150](https://github.com/Microsoft/vscode-python/issues/7150)) +1. Fix for testing code lenses don't show for remote sessions to a directory symlink. + ([#7443](https://github.com/Microsoft/vscode-python/issues/7443)) +1. Fix for discover test per folder icon is missing in multi-root workspace after upgrade. + ([#7870](https://github.com/Microsoft/vscode-python/issues/7870)) +1. Fix for clicking on a test in the Test Explorer does not navigate to the correct test. + ([#8448](https://github.com/Microsoft/vscode-python/issues/8448)) +1. Fix for if multiple tests have the same name, only one is run. + ([#8761](https://github.com/Microsoft/vscode-python/issues/8761)) +1. Fix for test failure is reported as a compile error. + ([#9640](https://github.com/Microsoft/vscode-python/issues/9640)) +1. Fix for discovering tests immediately after interpreter change often fails. + ([#9854](https://github.com/Microsoft/vscode-python/issues/9854)) +1. Fix for unittest module invoking wrong TestCase. + ([#10972](https://github.com/Microsoft/vscode-python/issues/10972)) +1. Fix for unable to navigate to test function. + ([#11866](https://github.com/Microsoft/vscode-python/issues/11866)) +1. Fix for running test fails trying to access non-existing file. + ([#12403](https://github.com/Microsoft/vscode-python/issues/12403)) +1. Fix for code lenses don't work after opening files from different projects in workspace. + ([#12995](https://github.com/Microsoft/vscode-python/issues/12995)) +1. Fix for the pytest icons keep spinning when run Test Method. + ([#13285](https://github.com/Microsoft/vscode-python/issues/13285)) +1. Test for any functionality related to testing doesn't work if language server is set to none. + ([#13713](https://github.com/Microsoft/vscode-python/issues/13713)) +1. Fix for cannot configure PyTest from UI. + ([#13916](https://github.com/Microsoft/vscode-python/issues/13916)) +1. Fix for test icons not updating when using pytest. + ([#15260](https://github.com/Microsoft/vscode-python/issues/15260)) +1. Fix for debugging tests is returning errors due to "unsupported status". + ([#15736](https://github.com/Microsoft/vscode-python/issues/15736)) +1. Removes `"request": "test"` as a config option. This can now be done with `"purpose": ["debug-test"]`. + ([#15790](https://github.com/Microsoft/vscode-python/issues/15790)) +1. Fix for "There was an error in running the tests" when stopping debugger. + ([#16475](https://github.com/Microsoft/vscode-python/issues/16475)) +1. Use the vscode API appropriately to find out what terminal is being used. + ([#16577](https://github.com/Microsoft/vscode-python/issues/16577)) +1. Fix unittest discovery. (thanks [JulianEdwards](https://github.com/bigjools)) + ([#16593](https://github.com/Microsoft/vscode-python/issues/16593)) +1. Fix run `installPythonLibs` error in windows. + ([#16844](https://github.com/Microsoft/vscode-python/issues/16844)) +1. Fix for test welcome screen flashes on refresh. + ([#16855](https://github.com/Microsoft/vscode-python/issues/16855)) +1. Show re-run failed test button only when there are failed tests. + ([#16856](https://github.com/Microsoft/vscode-python/issues/16856)) +1. Triggering test refresh shows progress indicator. + ([#16891](https://github.com/Microsoft/vscode-python/issues/16891)) +1. Fix environment sorting for the `Python: Select Interpreter` command. + (thanks [Marc Mueller](https://github.com/cdce8p)) + ([#16893](https://github.com/Microsoft/vscode-python/issues/16893)) +1. Fix for unittest not getting discovered in all cases. + ([#16902](https://github.com/Microsoft/vscode-python/issues/16902)) +1. Don't show full path in the description for each test node. + ([#16927](https://github.com/Microsoft/vscode-python/issues/16927)) +1. Fix for no notification shown if test framework is not configured and run all tests is called. + ([#16941](https://github.com/Microsoft/vscode-python/issues/16941)) +1. In experiments service don't always `await` on `initialfetch` which can be slow depending on the network. + ([#16959](https://github.com/Microsoft/vscode-python/issues/16959)) +1. Ensure 2.7 unittest still work with new test support. + ([#16962](https://github.com/Microsoft/vscode-python/issues/16962)) +1. Fix issue with parsing test run ids for reporting test status. + ([#16963](https://github.com/Microsoft/vscode-python/issues/16963)) +1. Fix cell magics, line magics, and shell escaping in jupyter notebooks to not show error diagnostics. + ([#17058](https://github.com/Microsoft/vscode-python/issues/17058)) +1. Fix for testing ui update issue when `pytest` parameter has '/'. + ([#17079](https://github.com/Microsoft/vscode-python/issues/17079)) + +### Code Health + +1. Remove nose test support. + ([#16371](https://github.com/Microsoft/vscode-python/issues/16371)) +1. Remove custom start page experience in favor of VSCode's built-in walkthrough support. + ([#16453](https://github.com/Microsoft/vscode-python/issues/16453)) +1. Run auto-selection only once, and return the cached value for subsequent calls. + ([#16735](https://github.com/Microsoft/vscode-python/issues/16735)) +1. Add telemetry for when an interpreter gets auto-selected. + ([#16764](https://github.com/Microsoft/vscode-python/issues/16764)) +1. Remove pre-existing environment sorting algorithm and old rule-based auto-selection logic. + ([#16935](https://github.com/Microsoft/vscode-python/issues/16935)) +1. Add API to run code after extension activation. + ([#16983](https://github.com/Microsoft/vscode-python/issues/16983)) +1. Add telemetry sending time it took to load data from experiment service. + ([#17011](https://github.com/Microsoft/vscode-python/issues/17011)) +1. Improve reliability of virtual env tests and disable poetry watcher tests. + ([#17088](https://github.com/Microsoft/vscode-python/issues/17088)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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/), + [poetry](https://pypi.org/project/poetry/), + [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: + [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! + +## 2021.8.3 (23 August 2021) + +### Fixes + +1. Update `vsce` to latest to fix metadata in VSIX for web extension. + ([#17049](https://github.com/Microsoft/vscode-python/issues/17049)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.8.2 (19 August 2021) + +### Enhancements + +1. Add a basic web extension bundle. + ([#16869](https://github.com/Microsoft/vscode-python/issues/16869)) +1. Add basic Pylance support to the web extension. + ([#16870](https://github.com/Microsoft/vscode-python/issues/16870)) + +### Code Health + +1. Update telemetry client to support browser, plumb to Pylance. + ([#16871](https://github.com/Microsoft/vscode-python/issues/16871)) +1. Refactor language server middleware to work in the browser. + ([#16872](https://github.com/Microsoft/vscode-python/issues/16872)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.8.1 (6 August 2021) + +### Fixes + +1. Fix random delay before running python code. + ([#16768](https://github.com/Microsoft/vscode-python/issues/16768)) +1. Fix the order of default unittest arguments. + (thanks [Nikolay Kondratyev](https://github.com/kondratyev-nv/)) + ([#16882](https://github.com/Microsoft/vscode-python/issues/16882)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.8.0 (5 August 2021) + +### Enhancements + +1. Add new getting started page using VS Code's API to replace our custom start page. + ([#16678](https://github.com/Microsoft/vscode-python/issues/16678)) +1. Replace deprecated vscode-test with @vscode/test-electron for CI. (thanks [iChenLei](https://github.com/iChenLei)) + ([#16765](https://github.com/Microsoft/vscode-python/issues/16765)) + +### Code Health + +1. Sort Settings Alphabetically. (thanks [bfarahdel](https://github.com/bfarahdel)) + ([#8406](https://github.com/Microsoft/vscode-python/issues/8406)) +1. Changed default language server to `Pylance` for extension development. (thanks [jasleen101010](https://github.com/jasleen101010)) + ([#13007](https://github.com/Microsoft/vscode-python/issues/13007)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.7.2 (23 July 2021) + +### Enhancements + +1. Update `debugpy` with fix for https://github.com/microsoft/debugpy/issues/669. + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.7.1 (21 July 2021) + +### Enhancements + +1. Update `debugpy` to the latest version. + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.7.0 (20 July 2021) + +### Enhancements + +1. Support starting a TensorBoard session with a remote URL hosting log files. + ([#16461](https://github.com/Microsoft/vscode-python/issues/16461)) +1. Sort environments in the selection quickpick by assumed usefulness. + ([#16520](https://github.com/Microsoft/vscode-python/issues/16520)) + +### Fixes + +1. Add link to docs page on how to install the Python extension to README. (thanks [KamalSinghKhanna](https://github.com/KamalSinghKhanna)) + ([#15199](https://github.com/Microsoft/vscode-python/issues/15199)) +1. Make test explorer only show file/folder names on nodes. + (thanks [bobwalker99](https://github.com/bobwalker99)) + ([#16368](https://github.com/Microsoft/vscode-python/issues/16368)) +1. Ensure we dispose restart command registration before we create a new instance of Jedi LS. + ([#16441](https://github.com/Microsoft/vscode-python/issues/16441)) +1. Ensure `shellIdentificationSource` is set correctly. (thanks [intrigus-lgtm](https://github.com/intrigus-lgtm)) + ([#16517](https://github.com/Microsoft/vscode-python/issues/16517)) +1. Clear Notebook Cell diagnostics when deleting a cell or closing a notebook. + ([#16528](https://github.com/Microsoft/vscode-python/issues/16528)) +1. The `poetryPath` setting will correctly apply system variable substitutions. (thanks [Anthony Shaw](https://github.com/tonybaloney)) + ([#16607](https://github.com/Microsoft/vscode-python/issues/16607)) +1. The Jupyter Notebook extension will install any missing dependencies using Poetry or Pipenv if those are the selected environments. (thanks [Anthony Shaw](https://github.com/tonybaloney)) + ([#16615](https://github.com/Microsoft/vscode-python/issues/16615)) +1. Ensure we block on autoselection when no interpreter is explictly set by user. + ([#16723](https://github.com/Microsoft/vscode-python/issues/16723)) +1. Fix autoselection when opening a python file directly. + ([#16733](https://github.com/Microsoft/vscode-python/issues/16733)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.6.0 (16 June 2021) + +### Enhancements + +1. Improved telemetry around the availability of `pip` for installation of Jupyter dependencies. + ([#15937](https://github.com/Microsoft/vscode-python/issues/15937)) +1. Move the Jupyter extension from being a hard dependency to an optional one, and display an informational prompt if Jupyter commands try to be executed from the Start Page. + ([#16102](https://github.com/Microsoft/vscode-python/issues/16102)) +1. Add an `enumDescriptions` key under the `python.languageServer` setting to describe all language server options. + ([#16141](https://github.com/Microsoft/vscode-python/issues/16141)) +1. Ensure users upgrade to v0.2.0 of the torch-tb-profiler TensorBoard plugin to access jump-to-source functionality. + ([#16330](https://github.com/Microsoft/vscode-python/issues/16330)) +1. Added `python.defaultInterpreterPath` setting at workspace level when in `pythonDeprecatePythonPath` experiment. + ([#16485](https://github.com/Microsoft/vscode-python/issues/16485)) +1. Added default Interpreter path entry at the bottom of the interpreter list. + ([#16485](https://github.com/Microsoft/vscode-python/issues/16485)) +1. Remove execution isolation script used to run tools. + ([#16485](https://github.com/Microsoft/vscode-python/issues/16485)) +1. Show `python.pythonPath` deprecation prompt when in `pythonDeprecatePythonPath` experiment. + ([#16485](https://github.com/Microsoft/vscode-python/issues/16485)) +1. Do not show safety prompt before auto-selecting a workspace interpreter. + ([#16485](https://github.com/Microsoft/vscode-python/issues/16485)) +1. Assume workspace interpreters are safe to execute for discovery. + ([#16485](https://github.com/Microsoft/vscode-python/issues/16485)) + +### Fixes + +1. Fixes a bug in the bandit linter where messages weren't being propagated to the editor. + (thanks [Anthony Shaw](https://github.com/tonybaloney)) + ([#15561](https://github.com/Microsoft/vscode-python/issues/15561)) +1. Workaround existing MIME type misconfiguration on Windows preventing TensorBoard from loading when starting TensorBoard. + ([#16072](https://github.com/Microsoft/vscode-python/issues/16072)) +1. Changed the version of npm to version 6 instead of 7 in the lockfile. + ([#16208](https://github.com/Microsoft/vscode-python/issues/16208)) +1. Ensure selected interpreter doesn't change when the extension is starting up and in experiment. + ([#16291](https://github.com/Microsoft/vscode-python/issues/16291)) +1. Fix issue with sys.prefix when getting environment details. + ([#16355](https://github.com/Microsoft/vscode-python/issues/16355)) +1. Activate the extension when selecting the command `Clear Internal Extension Cache (python.clearPersistentStorage)`. + ([#16397](https://github.com/Microsoft/vscode-python/issues/16397)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.5.2 (14 May 2021) + +### Fixes + +1. Ensure Pylance is used with Python 2 if explicitly chosen + ([#16246](https://github.com/microsoft/vscode-python/issues/16246)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.5.1 (13 May 2021) + +### Fixes + +1. Allow Pylance to be used with Python 2 if explicitly chosen + ([#16204](https://github.com/microsoft/vscode-python/issues/16204)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.5.0 (10 May 2021) + +### Enhancements + +1. In an integrated TensorBoard session, if the jump to source request is for a file that does not exist on disk, allow the user to manually specify the file using the system file picker. + ([#15695](https://github.com/Microsoft/vscode-python/issues/15695)) +1. Allow running tests for all files within directories from test explorer. + (thanks [Vladimir Kotikov](https://github.com/vladimir-kotikov)) + ([#15862](https://github.com/Microsoft/vscode-python/issues/15862)) +1. Reveal selection in editor after jump to source command. (thanks [Wenlu Wang](https://github.com/Kingwl)) + ([#15924](https://github.com/Microsoft/vscode-python/issues/15924)) +1. Add support for debugger code reloading. + ([#16029](https://github.com/Microsoft/vscode-python/issues/16029)) +1. Add Python: Refresh TensorBoard command, keybinding and editor title button to reload TensorBoard (equivalent to browser refresh). + ([#16053](https://github.com/Microsoft/vscode-python/issues/16053)) +1. Automatically indent following `match` and `case` statements. (thanks [Marc Mueller](https://github.com/cdce8p)) + ([#16104](https://github.com/Microsoft/vscode-python/issues/16104)) +1. Bundle Pylance with the extension as an optional dependency. + ([#16116](https://github.com/Microsoft/vscode-python/issues/16116)) +1. Add a "Default" language server option, which dynamically chooses which language server to use. + ([#16157](https://github.com/Microsoft/vscode-python/issues/16157)) + +### Fixes + +1. Stop `unittest.TestCase` appearing as a test suite in the test explorer tree. + (thanks [Bob](https://github.com/bobwalker99)). + ([#15681](https://github.com/Microsoft/vscode-python/issues/15681)) +1. Support `~` in WORKON_HOME and venvPath setting when in discovery experiment. + ([#15788](https://github.com/Microsoft/vscode-python/issues/15788)) +1. Fix TensorBoard integration in Remote-SSH by auto-configuring port forwards. + ([#15807](https://github.com/Microsoft/vscode-python/issues/15807)) +1. Ensure venvPath and venvFolders setting can only be set at User or Remote settings. + ([#15947](https://github.com/Microsoft/vscode-python/issues/15947)) +1. Added compatability with pypy3.7 interpreter. + (thanks [Oliver Margetts](https://github.com/olliemath)) + ([#15968](https://github.com/Microsoft/vscode-python/issues/15968)) +1. Revert linter installation prompt removal. + ([#16027](https://github.com/Microsoft/vscode-python/issues/16027)) +1. Ensure that `dataclasses` is installed when using Jedi LSP. + ([#16119](https://github.com/Microsoft/vscode-python/issues/16119)) + +### Code Health + +1. Log the failures when checking whether certain modules are installed or getting their version information. + ([#15837](https://github.com/Microsoft/vscode-python/issues/15837)) +1. Better logging (telemetry) when installation of Python packages fail. + ([#15933](https://github.com/Microsoft/vscode-python/issues/15933)) +1. Ensure npm packave `canvas` is setup as an optional dependency. + ([#16127](https://github.com/Microsoft/vscode-python/issues/16127)) +1. Add ability for Jupyter extension to pass addtional installer arguments. + ([#16131](https://github.com/Microsoft/vscode-python/issues/16131)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.4.0 (19 April 2021) + +### Enhancements + +1. Add new command to report an Issue using the vscode-python template. + ([#1119](https://github.com/microsoft/vscode-python/issues/1119)) +1. Highlight `.pypirc`, `.pep8`, and `.pylintrc` as ini-files. (thanks [Jan Pilzer](https://github.com/Hirse)) + ([#11250](https://github.com/Microsoft/vscode-python/issues/11250)) +1. Added `python.linting.cwd` to change the working directory of the linters. (thanks [Matthew Shirley](https://github.com/matthewshirley)) + ([#15170](https://github.com/Microsoft/vscode-python/issues/15170)) +1. Remove prompt to install a linter when none are available. + ([#15465](https://github.com/Microsoft/vscode-python/issues/15465)) +1. Add jump to source integration with the PyTorch profiler TensorBoard plugin during TensorBoard sessions. + ([#15641](https://github.com/Microsoft/vscode-python/issues/15641)) +1. Drop prompt being displayed on first extension launch with a tip or a survey. + ([#15647](https://github.com/Microsoft/vscode-python/issues/15647)) +1. Use the updated logic for normalizing code sent to REPL as the default behavior. + ([#15649](https://github.com/Microsoft/vscode-python/issues/15649)) +1. Open TensorBoard webview panel in the active viewgroup on the first launch or the last viewgroup that it was moved to. + ([#15708](https://github.com/Microsoft/vscode-python/issues/15708)) +1. Support discovering Poetry virtual environments when in discovery experiment. + ([#15765](https://github.com/Microsoft/vscode-python/issues/15765)) +1. Install dev tools using Poetry when the poetry environment related to current folder is selected when in discovery experiment. + ([#15786](https://github.com/Microsoft/vscode-python/issues/15786)) +1. Add a refresh icon next to interpreter list. + ([#15868](https://github.com/Microsoft/vscode-python/issues/15868)) +1. Added command `Python: Clear internal extension cache` to clear extension related cache. + ([#15883](https://github.com/Microsoft/vscode-python/issues/15883)) + +### Fixes + +1. Fix `python.poetryPath` setting for installer on Windows. + ([#9672](https://github.com/Microsoft/vscode-python/issues/9672)) +1. Prevent mypy errors for other files showing in current file. + (thanks [Steve Dignam](https://github.com/sbdchd)) + ([#10190](https://github.com/Microsoft/vscode-python/issues/10190)) +1. Update pytest results when debugging. (thanks [djplt](https://github.com/djplt)) + ([#15353](https://github.com/Microsoft/vscode-python/issues/15353)) +1. Ensure release level is set when using new environment discovery component. + ([#15462](https://github.com/Microsoft/vscode-python/issues/15462)) +1. Ensure right environment is activated in the terminal when installing Python packages. + ([#15503](https://github.com/Microsoft/vscode-python/issues/15503)) +1. Update nosetest results when debugging. (thanks [djplt](https://github.com/djplt)) + ([#15642](https://github.com/Microsoft/vscode-python/issues/15642)) +1. Ensure any stray jedi process is terminated on language server dispose. + ([#15644](https://github.com/Microsoft/vscode-python/issues/15644)) +1. Fix README image indent for VSCode extension page. (thanks [Johnson](https://github.com/j3soon/)) + ([#15662](https://github.com/Microsoft/vscode-python/issues/15662)) +1. Run `conda update` and not `conda install` when installing a compatible version of the `tensorboard` package. + ([#15778](https://github.com/Microsoft/vscode-python/issues/15778)) +1. Temporarily fix support for folders in interpreter path setting. + ([#15782](https://github.com/Microsoft/vscode-python/issues/15782)) +1. In completions.py: jedi.api.names has been deprecated, switch to new syntax. + (thanks [moselhy](https://github.com/moselhy)). + ([#15791](https://github.com/Microsoft/vscode-python/issues/15791)) +1. Fixes activation of prefixed conda environments. + ([#15823](https://github.com/Microsoft/vscode-python/issues/15823)) + +### Code Health + +1. Deprecating on-type line formatter since it isn't used in newer Language servers. + ([#15709](https://github.com/Microsoft/vscode-python/issues/15709)) +1. Removing old way of feature deprecation where we showed notification for each feature we deprecated. + ([#15714](https://github.com/Microsoft/vscode-python/issues/15714)) +1. Remove unused code from extension. + ([#15717](https://github.com/Microsoft/vscode-python/issues/15717)) +1. Add telemetry for identifying torch.profiler users. + ([#15825](https://github.com/Microsoft/vscode-python/issues/15825)) +1. Update notebook code to not use deprecated .cells function on NotebookDocument. + ([#15885](https://github.com/Microsoft/vscode-python/issues/15885)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.3.1 (23 March 2021) + +### Fixes + +1. Fix link to create a new Jupyter notebook in Python start page. + ([#15621](https://github.com/Microsoft/vscode-python/issues/15621)) +1. Upgrade to latest `jedi-language-server` and use it for python >= 3.6. Use `jedi<0.18` for python 2.7 and <=3.5. + ([#15724](https://github.com/Microsoft/vscode-python/issues/15724)) +1. Check if Python executable file exists instead of launching the Python process. + ([#15725](https://github.com/Microsoft/vscode-python/issues/15725)) +1. Fix for Go to definition needs to be pressed twice. + (thanks [djplt](https://github.com/djplt)) + ([#15727](https://github.com/Microsoft/vscode-python/issues/15727)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.3.0 (16 March 2021) + +### Enhancements + +1. Activate the extension when the following files are found: `Pipfile`, `setup.py`, `requirements.txt`, `manage.py`, `app.py` + (thanks [Dhaval Soneji](https://github.com/soneji)) + ([#4765](https://github.com/Microsoft/vscode-python/issues/4765)) +1. Add optional user-level `python.tensorBoard.logDirectory` setting. When starting a TensorBoard session, use this setting if it is present instead of prompting the user to select a log directory. + ([#15476](https://github.com/Microsoft/vscode-python/issues/15476)) + +### Fixes + +1. Fix nosetests to run tests only once. (thanks [djplt](https://github.com/djplt)) + ([#6043](https://github.com/Microsoft/vscode-python/issues/6043)) +1. Make on-enter behaviour after `raise` much more like that of `return`, fixing + handling in the case of pressing enter to wrap the parentheses of an exception + call. + (thanks [PeterJCLaw](https://github.com/PeterJCLaw)) + ([#10583](https://github.com/Microsoft/vscode-python/issues/10583)) +1. Add configuration debugpyPath. (thanks [djplt](https://github.com/djplt)) + ([#14631](https://github.com/Microsoft/vscode-python/issues/14631)) +1. Fix Mypy linter pointing to wrong column number (off by one). + (thanks [anttipessa](https://github.com/anttipessa/), [haalto](https://github.com/haalto/), [JeonCD](https://github.com/JeonCD/) and [junskU](https://github.com/junskU)) + ([#14978](https://github.com/Microsoft/vscode-python/issues/14978)) +1. Show each python.org install only once on Mac when in discovery experiment. + ([#15302](https://github.com/Microsoft/vscode-python/issues/15302)) +1. All relative interpreter path reported start with `~` when in discovery experiment. + ([#15312](https://github.com/Microsoft/vscode-python/issues/15312)) +1. Remove FLASK_DEBUG from flask debug configuration to allow reload. + ([#15373](https://github.com/Microsoft/vscode-python/issues/15373)) +1. Install using pipenv only if the selected environment is pipenv which is related to workspace folder, when in discovery experiment. + ([#15489](https://github.com/Microsoft/vscode-python/issues/15489)) +1. Fixes issue with detecting new installations of Windows Store python. + ([#15541](https://github.com/Microsoft/vscode-python/issues/15541)) +1. Add `cached-property` package to bundled python packages. This is needed by `jedi-language-server` running on `python 3.6` and `python 3.7`. + ([#15566](https://github.com/Microsoft/vscode-python/issues/15566)) +1. Remove limit on workspace symbols when using Jedi language server. + ([#15576](https://github.com/Microsoft/vscode-python/issues/15576)) +1. Use shorter paths for python interpreter when possible. + ([#15580](https://github.com/Microsoft/vscode-python/issues/15580)) +1. Ensure that jedi language server uses jedi shipped with the extension. + ([#15586](https://github.com/Microsoft/vscode-python/issues/15586)) +1. Updates to Proposed API, and fix the failure in VS Code Insider tests. + ([#15638](https://github.com/Microsoft/vscode-python/issues/15638)) + +### Code Health + +1. Add support for "Trusted Workspaces". + + "Trusted Workspaces" is an upcoming feature in VS Code. (See: + https://github.com/microsoft/vscode/issues/106488.) For now you need + the following for the experience: + + - the latest VS Code Insiders + - add `"workspace.trustEnabled": true` to your user settings.json + + At that point, when the Python extension would normally activate, VS Code + will prompt you about whether or not the current workspace is trusted. + If not then the extension will be disabled (but only for that workspace). + As soon as the workspace is marked as trusted, the extension will + activate. + ([#15525](https://github.com/Microsoft/vscode-python/issues/15525)) + +1. Updates to the VSCode Notebook API. + ([#15567](https://github.com/Microsoft/vscode-python/issues/15567)) +1. Fix failing smoke tests on CI. + ([#15573](https://github.com/Microsoft/vscode-python/issues/15573)) +1. Update VS Code engine to 1.54.0 + ([#15604](https://github.com/Microsoft/vscode-python/issues/15604)) +1. Use `onReady` method available on language client to ensure language server is ready. + ([#15612](https://github.com/Microsoft/vscode-python/issues/15612)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.2.4 (9 March 2021) + +### Fixes + +1. Update to latest VSCode Notebook API. + ([#15415](https://github.com/Microsoft/vscode-python/issues/15415)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.2.3 (8 March 2021) + +### Fixes + +1. Add event handlers to stream error events to prevent process from exiting due to errors in process stdout & stderr streams. + ([#15395](https://github.com/Microsoft/vscode-python/issues/15395)) + +### 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/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [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! + +## 2021.2.2 (5 March 2021) + +### Fixes + +1. Fixes issue with Jedi Language Server telemetry. + ([#15419](https://github.com/microsoft/vscode-python/issues/15419)) + +### 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! + +## 2021.2.1 (19 February 2021) + +### Fixes + +1. Fix for missing pyenv virtual environments from selectable environments. + ([#15439](https://github.com/Microsoft/vscode-python/issues/15439)) +1. Register Jedi regardless of what language server is configured. + ([#15452](https://github.com/Microsoft/vscode-python/issues/15452)) + +### 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! + +## 2021.2.0 (17 February 2021) + +### Enhancements + +1. Use Language Server Protocol to work with Jedi. + ([#11995](https://github.com/Microsoft/vscode-python/issues/11995)) + +### Fixes + +1. Don't suggest insiders program nor show start page when in Codespaces. + ([#14833](https://github.com/Microsoft/vscode-python/issues/14833)) +1. Fix description of `Pyramid` debug config. + (thanks [vvijayalakshmi21](https://github.com/vvijayalakshmi21/)) + ([#5479](https://github.com/Microsoft/vscode-python/issues/5479)) +1. Refactored the Enable Linting command to provide the user with a choice of "Enable" or "Disable" linting to make it more intuitive. (thanks [henryboisdequin](https://github.com/henryboisdequin)) + ([#8800](https://github.com/Microsoft/vscode-python/issues/8800)) +1. Fix marketplace links in popups opening a non-browser VS Code instance in Codespaces. + ([#14264](https://github.com/Microsoft/vscode-python/issues/14264)) +1. Fixed the error command suggested when attempting to use "debug tests" configuration + (Thanks [Shahzaib paracha](https://github.com/ShahzaibParacha)) + ([#14729](https://github.com/Microsoft/vscode-python/issues/14729)) +1. Single test run fails sometimes if there is an error in unrelated file imported during discovery. + (thanks [Szymon Janota](https://github.com/sjanota/)) + ([#15147](https://github.com/Microsoft/vscode-python/issues/15147)) +1. Re-enable localization on the start page. It was accidentally + disabled in October when the Jupyter extension was split out. + ([#15232](https://github.com/Microsoft/vscode-python/issues/15232)) +1. Ensure target environment is activated in the terminal when running install scripts. + ([#15285](https://github.com/Microsoft/vscode-python/issues/15285)) +1. Allow support for using notebook APIs in the VS code stable build. + ([#15364](https://github.com/Microsoft/vscode-python/issues/15364)) + +### Code Health + +1. Raised the minimum required VS Code version to 1.51. + ([#15237](https://github.com/Microsoft/vscode-python/issues/15237)) + +### 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! + +## 2021.1.0 (21 January 2021) + +### Enhancements + +1. Remove code snippets (you can copy the + [old snippets](https://github.com/microsoft/vscode-python/blob/2020.12.424452561/snippets/python.json) + and use them as + [your own snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets)). + ([#14781](https://github.com/Microsoft/vscode-python/issues/14781)) +1. Add PYTHONPATH to the language server settings response. + ([#15106](https://github.com/Microsoft/vscode-python/issues/15106)) +1. Integration with the bandit linter will highlight the variable, function or method for an issue instead of the entire line. + Requires latest version of the bandit package to be installed. + (thanks [Anthony Shaw](https://github.com/tonybaloney)) + ([#15003](https://github.com/Microsoft/vscode-python/issues/15003)) +1. Translated some more of the Python Extension messages in Simplified Chinese. + (thanks [Shinoyasan](https://github.com/shinoyasan/)) + ([#15079](https://github.com/Microsoft/vscode-python/issues/15079)) +1. Update Simplified Chinese translation. + (thanks [Fiftysixtimes7](https://github.com/FiftysixTimes7)) + ([#14997](https://github.com/Microsoft/vscode-python/issues/14997)) + +### Fixes + +1. Fix environment variables not refreshing on env file edits. + ([#3805](https://github.com/Microsoft/vscode-python/issues/3805)) +1. fix npm audit[high]: [Remote Code Execution](npmjs.com/advisories/1548) + ([#14640](https://github.com/Microsoft/vscode-python/issues/14640)) +1. Ignore false positives when scraping environment variables. + ([#14812](https://github.com/Microsoft/vscode-python/issues/14812)) +1. Fix unittest discovery when using VS Code Insiders by using Inversify's `skipBaseClassChecks` option. + ([#14962](https://github.com/Microsoft/vscode-python/issues/14962)) +1. Make filtering in findInterpretersInDir() faster. + ([#14983](https://github.com/Microsoft/vscode-python/issues/14983)) +1. Remove the Buffer() is deprecated warning from Developer tools. ([#15045](https://github.com/microsoft/vscode-python/issues/15045)) + ([#15045](https://github.com/Microsoft/vscode-python/issues/15045)) +1. Add support for pytest 6 options. + ([#15094](https://github.com/Microsoft/vscode-python/issues/15094)) + +### Code Health + +1. Update to Node 12.20.0. + ([#15046](https://github.com/Microsoft/vscode-python/issues/15046)) + +### 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.12.2 (15 December 2020) + +### Fixes + +1. Only activate discovery component when in experiment. + ([#14977](https://github.com/Microsoft/vscode-python/issues/14977)) + +### 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.12.1 (15 December 2020) + +### Fixes + +1. Fix for extension loading issue in the latest release. + ([#14977](https://github.com/Microsoft/vscode-python/issues/14977)) + +### 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.12.0 (14 December 2020) + +### Enhancements + +1. FastAPI debugger feature. + (thanks [Marcelo Trylesinski](https://github.com/kludex/)!) + ([#14247](https://github.com/Microsoft/vscode-python/issues/14247)) +1. Put linter prompt behind an experiment flag. + ([#14760](https://github.com/Microsoft/vscode-python/issues/14760)) +1. Add Python: Launch TensorBoard command behind an experiment. + ([#14806](https://github.com/Microsoft/vscode-python/issues/14806)) +1. Detect tfevent files in workspace and prompt to launch native TensorBoard session. + ([#14807](https://github.com/Microsoft/vscode-python/issues/14807)) +1. Use default color for "Select Python interpreter" on the status bar. + (thanks [Daniel Rodriguez](https://github.com/danielfrg)!) + ([#14859](https://github.com/Microsoft/vscode-python/issues/14859)) +1. Experiment to use the new environment discovery module. + ([#14868](https://github.com/Microsoft/vscode-python/issues/14868)) +1. Add experimentation API support for Pylance. + ([#14895](https://github.com/Microsoft/vscode-python/issues/14895)) + +### Fixes + +1. Format `.pyi` files correctly when using Black. + (thanks [Steve Dignam](https://github.com/sbdchd)!) + ([#13341](https://github.com/Microsoft/vscode-python/issues/13341)) +1. Add `node-loader` to support `webpack` for `fsevents` package. + ([#14664](https://github.com/Microsoft/vscode-python/issues/14664)) +1. Don't show play icon in diff editor. + (thanks [David Sanders](https://github.com/dsanders11)!) + ([#14800](https://github.com/Microsoft/vscode-python/issues/14800)) +1. Do not show "You need to select a Python interpreter before you start debugging" when "python" in debug configuration is invalid. + ([#14814](https://github.com/Microsoft/vscode-python/issues/14814)) +1. Fix custom language server message handlers being registered too late in startup. + ([#14893](https://github.com/Microsoft/vscode-python/issues/14893)) + +### Code Health + +1. Modified the errors generated when `launch.json` is not properly configured to be more specific about which fields are missing. + (thanks [Shahzaib Paracha](https://github.com/ShahzaibP)!) + ([#14739](https://github.com/Microsoft/vscode-python/issues/14739)) + +### 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.11.1 (17 November 2020) + +### Enhancements + +1. Replaced "pythonPath" debug configuration property with "python". + ([#12462](https://github.com/Microsoft/vscode-python/issues/12462)) + +### Fixes + +1. Fix for Process Id Picker no longer showing up + ([#14678](https://github.com/Microsoft/vscode-python/issues/14678))) +1. Fix workspace symbol searching always returning empty. + ([#14727](https://github.com/Microsoft/vscode-python/issues/14727)) + +### 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.11.0 (11 November 2020) + +### Enhancements + +1. Update shipped debugger wheels to python 3.8. + ([#14614](https://github.com/Microsoft/vscode-python/issues/14614)) + +### Fixes + +1. Update the logic for parsing and sending selected code to the REPL. + ([#14048](https://github.com/Microsoft/vscode-python/issues/14048)) +1. Fix "TypeError: message must be set" error when debugging with `pytest`. + ([#14067](https://github.com/Microsoft/vscode-python/issues/14067)) +1. When sending code to the REPL, read input from `sys.stdin` instead of passing it as an argument. + ([#14471](https://github.com/Microsoft/vscode-python/issues/14471)) + +### Code Health + +1. Code for Jupyter Notebooks support has been refactored into the Jupyter extension, which is now a dependency for the Python 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.10.0 (27 October 2020) + +### Enhancements + +1. `debugpy` updated to latest stable version. +1. Make data viewer openable from the variables window context menu while debugging. + ([#14406](https://github.com/Microsoft/vscode-python/issues/14406)) +1. Do not opt users out of the insiders program if they have a stable version installed. + ([#14090](https://github.com/Microsoft/vscode-python/issues/14090)) + +### Fixes + +1. Make sure not to set `__file__` unless necessary as this can mess up some modules (like multiprocessing). + ([#12530](https://github.com/Microsoft/vscode-python/issues/12530)) +1. Fix isolate script to only remove current working directory. + ([#13942](https://github.com/Microsoft/vscode-python/issues/13942)) +1. Make sure server name and kernel name show up when connecting. + ([#13955](https://github.com/Microsoft/vscode-python/issues/13955)) +1. Have Custom Editors load on editor show unless autostart is disabled. + ([#14016](https://github.com/Microsoft/vscode-python/issues/14016)) +1. For exporting, first check the notebook or interactive window interpreter before the jupyter selected interpreter. + ([#14143](https://github.com/Microsoft/vscode-python/issues/14143)) +1. Fix interactive debugging starting (trimQuotes error). + ([#14212](https://github.com/Microsoft/vscode-python/issues/14212)) +1. Use the kernel defined in the metadata of Notebook instead of using the default workspace interpreter. + ([#14213](https://github.com/Microsoft/vscode-python/issues/14213)) +1. Fix latex output not showing up without a 'display' call. + ([#14216](https://github.com/Microsoft/vscode-python/issues/14216)) +1. Fix markdown cell marker when exporting a notebook to a Python script. + ([#14359](https://github.com/Microsoft/vscode-python/issues/14359)) + +### Code Health + +1. Add Windows unit tests to the PR validation pipeline. + ([#14013](https://github.com/Microsoft/vscode-python/issues/14013)) +1. Functional test failures related to kernel ports overlapping. + ([#14290](https://github.com/Microsoft/vscode-python/issues/14290)) +1. Change message from `IPython kernel` to `Jupyter kernel`. + ([#14309](https://github.com/Microsoft/vscode-python/issues/14309)) + +### 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.9.2 (6 October 2020) + +### Fixes + +1. Support nbconvert version 6+ for exporting notebooks to python code. + ([#14169](https://github.com/Microsoft/vscode-python/issues/14169)) +1. Do not escape output in the actual ipynb file. + ([#14182](https://github.com/Microsoft/vscode-python/issues/14182)) +1. Fix exporting from the interactive window. + ([#14210](https://github.com/Microsoft/vscode-python/issues/14210)) +1. Fix for CVE-2020-16977 + ([CVE-2020-16977](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2020-16977)) +1. Fix for CVE-2020-17163 + ([CVE-2020-17163](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2020-17163)) + +### 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.9.1 (29 September 2020) + +### Fixes + +1. Fix IPyKernel install issue with windows paths. + ([#13493](https://github.com/microsoft/vscode-python/issues/13493)) +1. Fix escaping of output to encode HTML chars correctly. + ([#5678](https://github.com/Microsoft/vscode-python/issues/5678)) + +### 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.9.0 (23 September 2020) + +### Enhancements + +1. Docstrings are added to `class` and `def` snippets (thanks [alannt777](https://github.com/alannt777/)). + ([#5578](https://github.com/Microsoft/vscode-python/issues/5578)) +1. Upgraded isort to `5.3.2`. + ([#12932](https://github.com/Microsoft/vscode-python/issues/12932)) +1. Remove default "--no-reload" from debug configurations. + (thanks [ian910297](https://github.com/ian910297)) + ([#13061](https://github.com/Microsoft/vscode-python/issues/13061)) +1. Update API to expose events for cell excecution and kernel restart. + ([#13306](https://github.com/Microsoft/vscode-python/issues/13306)) +1. Show a general warning prompt pointing to the upgrade guide when users attempt to run isort5 using deprecated settings. + ([#13716](https://github.com/Microsoft/vscode-python/issues/13716)) +1. Upgrade isort to `5.5.2`. + ([#13831](https://github.com/Microsoft/vscode-python/issues/13831)) +1. Enable custom editor support in stable VS code at 20%. + ([#13890](https://github.com/Microsoft/vscode-python/issues/13890)) +1. Upgraded to isort `5.5.3`. + ([#14027](https://github.com/Microsoft/vscode-python/issues/14027)) + +### Fixes + +1. Fixed the output being trimmed. Tables that start with empty space will now display correctly. + ([#10270](https://github.com/Microsoft/vscode-python/issues/10270)) +1. #11729 + Prevent test discovery from picking up stdout from low level file descriptors. + (thanks [Ryo Miyajima](https://github.com/sergeant-wizard)) + ([#11729](https://github.com/Microsoft/vscode-python/issues/11729)) +1. Fix opening new blank notebooks when using the VS code custom editor API. + ([#12245](https://github.com/Microsoft/vscode-python/issues/12245)) +1. Support starting kernels with the same directory as the notebook. + ([#12760](https://github.com/Microsoft/vscode-python/issues/12760)) +1. Fixed `Sort imports` command with setuptools version `49.2`. + ([#12949](https://github.com/Microsoft/vscode-python/issues/12949)) +1. Do not fail interpreter discovery if accessing Windows registry fails. + ([#12962](https://github.com/Microsoft/vscode-python/issues/12962)) +1. Show error output from nbconvert when exporting a notebook fails. + ([#13229](https://github.com/Microsoft/vscode-python/issues/13229)) +1. Prevent daemon from trying to prewarm an execution service. + ([#13258](https://github.com/Microsoft/vscode-python/issues/13258)) +1. Respect stop on error setting for executing cells in native notebook. + ([#13338](https://github.com/Microsoft/vscode-python/issues/13338)) +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. + ([#13409](https://github.com/Microsoft/vscode-python/issues/13409)) +1. Fix path to isolated script on Windows shell_exec. + ([#13493](https://github.com/Microsoft/vscode-python/issues/13493)) +1. Updating other cells with display.update does not work in native notebooks. + ([#13509](https://github.com/Microsoft/vscode-python/issues/13509)) +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. + ([#13520](https://github.com/Microsoft/vscode-python/issues/13520)) +1. Shift+enter should execute current cell and select the next cell. + ([#13553](https://github.com/Microsoft/vscode-python/issues/13553)) +1. Fixes typo in export command registration. + (thanks [Anton Kosyakov](https://github.com/akosyakov/)) + ([#13612](https://github.com/Microsoft/vscode-python/issues/13612)) +1. Fix the behavior of the 'python.showStartPage' setting. + ([#13706](https://github.com/Microsoft/vscode-python/issues/13706)) +1. Correctly install ipykernel when launching from an interpreter. + ([#13956](https://github.com/Microsoft/vscode-python/issues/13956)) +1. Backup on custom editors is being ignored. + ([#13981](https://github.com/Microsoft/vscode-python/issues/13981)) + +### Code Health + +1. Fix bandit issues in vscode_datascience_helpers. + ([#13103](https://github.com/Microsoft/vscode-python/issues/13103)) +1. Cast type to `any` to get around issues with `ts-node` (`ts-node` is used by `nyc` for code coverage). + ([#13411](https://github.com/Microsoft/vscode-python/issues/13411)) +1. Drop support for Python 3.5 (it reaches end-of-life on September 13, 2020 and isort 5 does not support it). + ([#13459](https://github.com/Microsoft/vscode-python/issues/13459)) +1. Fix nightly flake test issue with timeout waiting for kernel. + ([#13501](https://github.com/Microsoft/vscode-python/issues/13501)) +1. Disable sorting tests for Python 2.7 as isort5 is not compatible with Python 2.7. + ([#13542](https://github.com/Microsoft/vscode-python/issues/13542)) +1. Fix nightly flake test current directory failing test. + ([#13605](https://github.com/Microsoft/vscode-python/issues/13605)) +1. Rename the `master` branch to `main`. + ([#13645](https://github.com/Microsoft/vscode-python/issues/13645)) +1. Remove usage of the terms "blacklist" and "whitelist". + ([#13647](https://github.com/Microsoft/vscode-python/issues/13647)) +1. Fix a test failure and warning when running test adapter tests under pytest 5. + ([#13726](https://github.com/Microsoft/vscode-python/issues/13726)) +1. Remove unused imports from data science ipython test files. + ([#13729](https://github.com/Microsoft/vscode-python/issues/13729)) +1. Fix nighly failure with beakerx. + ([#13734](https://github.com/Microsoft/vscode-python/issues/13734)) + +## 2020.8.6 (15 September 2020) + +### Fixes + +1. Workaround problem caused by https://github.com/microsoft/vscode/issues/106547 + +## 2020.8.6 (15 September 2020) + +### Fixes + +1. Workaround problem caused by https://github.com/microsoft/vscode/issues/106547 + +### 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.5 (9 September 2020) + +### Fixes + +1. Experiments.json is now read from 'main' branch. + ([#13839](https://github.com/Microsoft/vscode-python/issues/13839)) + +### 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.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 @@ -9,7 +4505,8 @@ ([#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. (@BrandonLWhite) +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)) @@ -29,10 +4526,12 @@ ([#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 interative window. +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 @@ -40,6 +4539,8 @@ ([#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. @@ -85,6 +4586,10 @@ ([#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 @@ -115,6 +4620,10 @@ ([#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 @@ -2091,7 +6600,7 @@ part of! ([#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. +1. Add commands translation for Persian 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. @@ -2637,7 +7146,7 @@ part of! ([#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 master branch has the '-dev' suffix. +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)) @@ -4712,7 +9221,7 @@ part of! ([#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 `master` branch of ptvsd. +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). @@ -5583,7 +10092,7 @@ nearly as feature-rich and useful as it is. ([#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 `master` branch. +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)) @@ -5713,7 +10222,7 @@ his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! ([#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 master branch of PTVSD (experimental debugger), allowing tests to fail against the master branch of PTVSD. +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) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 18873b3ccf8a..f9ba8cf65f3e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,6 +1,9 @@ -This project has adopted the [Microsoft Open Source Code of -Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct -FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) -with any additional questions or comments. +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md deleted file mode 100644 index f6d3a03a943d..000000000000 --- a/CODING_STANDARDS.md +++ /dev/null @@ -1,55 +0,0 @@ -## Coding guidelines for TypeScript - -- The following standards are inspired from [Coding guidelines for TypeScript](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines) (which you should follow when something is not specified in this document, although any pre-existing practices in a file being edited trump either style guide). - -### Names - -- Use `PascalCase` for type names. -- Use `I` as a prefix for interface names only when an interface is implemented by a class. -- Use `PascalCase` for enum values. -- Use `camelCase` for function names. -- Use `camelCase` for property names and local variables. -- Do not use `_` as a prefix for private properties (unless used as backing properties). -- Use whole words in names when possible. - -### Types - -- Do not export types/functions unless you need to share it across multiple components. -- Do not introduce new types/values to the global namespace. -- Shared types should be defined in `types.ts`. - Within a file, type definitions should come first. - -### null and undefined - -Use undefined. Do not use null. - -### Comments - -- Comments must end with a period. -- Use JSDoc style comments for functions, interfaces, enums, and classes. - -### Strings - -Use single quotes for strings. - -### Imports - -- Use ES6 module imports. -- Do not use bare `import *`; all imports should either explicitly pull in an object or import an entire module, otherwise you're implicitly polluting the global namespace and making it difficult to figure out from code examination where a name originates from. - -### Style - -- Use `prettier` to format `TypeScript` and `JavaScript` code. -- Use arrow functions over anonymous function expressions. -- Always surround loop and conditional bodies with curly braces. Statements on the same line are allowed to omit braces. -- Open curly braces always go on the same line as whatever necessitates them. -- Parenthesized constructs should have no surrounding whitespace. -- A single space follows commas, colons, and semicolons in those constructs. For example: - - - `for (var i = 0, n = str.length; i < 10; i++) { }` - - `if (x < 10) { }` - - `function f(x: number, y: string): void { }` - -- `else` goes on the same line from the closing curly brace. -- Use 4 spaces per indentation. -- All files must end with an empty line. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc702e236bb4..c6c0998395b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,358 +1 @@ -# Contributing to the Python extension for Visual Studio Code - ---- - -| `release` branch | `master` branch | Nightly CI | coverage (`master` branch) | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -| [![Build Status](https://dev.azure.com/ms/vscode-python/_apis/build/status/CI?branchName=release)](https://dev.azure.com/ms/vscode-python/_build/latest?definitionId=88&branchName=release) | [![Build Status](https://dev.azure.com/ms/vscode-python/_apis/build/status/CI?branchName=master)](https://dev.azure.com/ms/vscode-python/_build/latest?definitionId=88&branchName=master) | [![Build Status](https://dev.azure.com/ms/vscode-python/_apis/build/status/Nightly%20Build?branchName=master)](https://dev.azure.com/ms/vscode-python/_build/latest?definitionId=85&branchName=master) | [![codecov](https://codecov.io/gh/microsoft/vscode-python/branch/master/graph/badge.svg)](https://codecov.io/gh/microsoft/vscode-python) | - -[[Development build](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix)] - ---- - -[For contributing to the [Microsoft Python Language Server](https://github.com/Microsoft/python-language-server) see its own repo; for [Pylance](https://github.com/microsoft/pylance-release) see its own repo; for [debugpy](https://github.com/microsoft/debugpy) see its own repo] - -## Contributing a pull request - -### Prerequisites - -1. [Node.js](https://nodejs.org/) 12.15 -1. [Python](https://www.python.org/) 2.7 or later -1. Windows, macOS, or Linux -1. [Visual Studio Code](https://code.visualstudio.com/) -1. The following VS Code extensions: - - [TSLint](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin) - - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) -1. Have an issue which has a "needs PR" label (feel free to indicate you would like to provide a PR for the issue so others don't work on it as well) - -### Setup - -```shell -git clone https://github.com/microsoft/vscode-python -cd vscode-python -npm ci -python3 -m venv .venv -# Activate the virtual environment as appropriate for your shell, For example, on bash it's ... -source .venv/bin/activate -# The Python code in the extension is formatted using Black. -python3 -m pip install black -# Install Python dependencies using `python3`. -# If you want to use a different interpreter then specify it in the -# CI_PYTHON_PATH environment variable. -npx gulp installPythonLibs -``` - -If you see warnings that `The engine "vscode" appears to be invalid.`, you can ignore these. - -### Incremental Build - -Run the `Compile` and `Hygiene` build Tasks from the [Run Build Task...](https://code.visualstudio.com/docs/editor/tasks) command picker (short cut `CTRL+SHIFT+B` or `⇧⌘B`). This will leave build and hygiene tasks running in the background and which will re-run as files are edited and saved. You can see the output from either task in the Terminal panel (use the selector to choose which output to look at). - -You can also compile from the command-line. For a full compile you can use: - -```shell -npx gulp prePublishNonBundle -``` - -For incremental builds you can use the following commands depending on your needs: - -```shell -npm run compile -npm run compile-webviews-watch # For data science (React Code) -``` - -Sometimes you will need to run `npm run clean` and even `rm -r out`. -This is especially true if you have added or removed files. - -### Errors and Warnings - -TypeScript errors and warnings will be displayed in the `Problems` window of Visual Studio Code. - -### Run dev build and validate your changes - -To test changes, open the `vscode-python` folder in VSCode, and select the workspace titled `vscode-python`. -Then, open the debug panel by clicking the `Run and Debug` icon on the sidebar, select the `Extension` -option from the top menu, and click start. A new window will launch with the title -`[Extension Development Host]`. - -### Running Unit Tests - -Note: Unit tests are those in files with extension `.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 -1. Run the Tests via the `Unit Tests` launch option. - -You can also run them from the command-line (after compiling): - -```shell -npm run test:unittests # runs all unit tests -npm run test:unittests -- --grep='' -``` - -_To run only a specific test suite for unit tests:_ -Alter the `launch.json` file in the `"Debug Unit Tests"` section by setting the `grep` field: - -```js - "args": [ - "--timeout=60000", - "--grep", "" - ], -``` - -...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/master/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/master/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 `master` 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/master/.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/master/.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/master/.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 `master` 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). +Please see [our wiki](https://github.com/microsoft/vscode-python/wiki) on how to contribute to this project. diff --git a/PYTHON_INTERACTIVE_TROUBLESHOOTING.md b/PYTHON_INTERACTIVE_TROUBLESHOOTING.md deleted file mode 100644 index 03688ee0a986..000000000000 --- a/PYTHON_INTERACTIVE_TROUBLESHOOTING.md +++ /dev/null @@ -1,66 +0,0 @@ -# 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 55b9e8e465f1..e9dd52a538cd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,44 @@ # 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.5), including features such as IntelliSense, linting, debugging, code navigation, code formatting, Jupyter notebook support, refactoring, variable explorer, test explorer, snippets, and more! +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 Python versions](https://devguide.python.org/versions/#supported-versions)), providing access points for extensions to seamlessly integrate and offer support for IntelliSense (Pylance), debugging (Python Debugger), formatting, linting, code navigation, refactoring, variable explorer, test explorer, environment management (**NEW** Python Environments Extension). + +## Support for [vscode.dev](https://vscode.dev/) + +The Python extension does offer [some support](https://github.com/microsoft/vscode-python/wiki/Partial-mode) when running on [vscode.dev](https://vscode.dev/) (which includes [github.dev](http://github.dev/)). This includes partial IntelliSense for open files in the editor. + + +## Installed extensions + +The Python extension will automatically install the following extensions by default to provide the best Python development experience in VS Code: + +- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) – performant Python language support +- [Python Debugger](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) – seamless debug experience with debugpy +- **(NEW)** [Python Environments](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs) – dedicated environment management (see below) + +These extensions are optional dependencies, meaning the Python extension will remain fully functional if they fail to be installed. Any or all of these extensions can be [disabled](https://code.visualstudio.com/docs/editor/extension-marketplace#_disable-an-extension) or [uninstalled](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) at the expense of some features. Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). + +### About the Python Environments Extension + +You may now see that the **Python Environments Extension** is installed for you, but it may or may not be "enabled" in your VS Code experience. Enablement is controlled by the setting `"python.useEnvironmentsExtension": true` (or `false`). + +- If you set this setting to `true`, you will manually opt in to using the Python Environments Extension for environment management. +- If you do not have this setting specified, you may be randomly assigned to have it turned on as we roll it out until it becomes the default experience for all users. + +The Python Environments Extension is still under active development and experimentation. Its goal is to provide a dedicated view and improved workflows for creating, deleting, and switching between Python environments, as well as managing packages. If you have feedback, please let us know via [issues](https://github.com/microsoft/vscode-python/issues). + +## Extensibility + +The Python extension provides pluggable access points for extensions that extend various feature areas to further improve your Python development experience. These extensions are all optional and depend on your project configuration and preferences. + +- [Python formatters](https://code.visualstudio.com/docs/python/formatting#_choose-a-formatter) +- [Python linters](https://code.visualstudio.com/docs/python/linting#_choose-a-linter) + +If you encounter issues with any of the listed extensions, please file an issue in its corresponding repo. ## 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 1.** [Install a supported version of Python on your system](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) (note: the system install of Python on macOS is not supported). +- **Step 2.** [Install the Python extension for Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-gallery). - **Step 3.** Open or create a Python file and start coding! ## Set up your environment @@ -14,21 +47,25 @@ A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marke - 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 +The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. + +- Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). + - Open or create a Jupyter Notebook file (.ipynb) and start coding in our Notebook Editor! - + For more information you can: @@ -43,31 +80,26 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | 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: Start Terminal 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`. +To see all available Python commands, open the Command Palette and type `Python`. For Jupyter extension commands, just type `Jupyter`. ## 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 +- [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/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 or pytest. +- [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 and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). -- [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 @@ -75,13 +107,13 @@ The extension is available in multiple languages: `de`, `en`, `es`, `fa`, `fr`, ## 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/master/CONTRIBUTING.md) for more details +- If you have a question about how to accomplish something with the extension, please [ask on our Discussions page](https://github.com/microsoft/vscode-python/discussions/categories/q-a). +- 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/master/CONTRIBUTING.md#development-process) + - 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 start a [new discussion](https://github.com/microsoft/vscode-python/discussions/categories/ideas). +- 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 @@ -89,6 +121,6 @@ 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` +learn more. This extension respects the `telemetry.telemetryLevel` setting which you can learn more about at https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000000..b1afe54cc555 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,11 @@ +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the [existing issues](https://github.com/microsoft/vscode-python/issues) before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. + +For help and questions about using this project, please see the [`python`+`visual-studio-code` labels on Stack Overflow](https://stackoverflow.com/questions/tagged/visual-studio-code+python) or the `#vscode` channel on the [`microsoft-python` server on Discord](https://aka.ms/python-discord-invite). + +## Microsoft Support Policy + +Support for this project is limited to the resources listed above. diff --git a/ThirdPartyNotices-Distribution.txt b/ThirdPartyNotices-Distribution.txt deleted file mode 100644 index 02b69b5d6c19..000000000000 --- a/ThirdPartyNotices-Distribution.txt +++ /dev/null @@ -1,15662 +0,0 @@ -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. - ---------------------------------------------------------- - -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 - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - ---------------------------------------------------------- - ---------------------------------------------------------- - -async-listener 0.6.10 - BSD-2-Clause -https://github.com/othiym23/async-listener#readme - -Copyright (c) 2013-2017, Forrest L Norvell -Copyright Joyent, Inc. and other Node contributors. - -BSD 2-Clause License - -Copyright (c) 2013-2017, Forrest L Norvell -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -cls-hooked 4.2.2 - BSD-2-Clause -https://github.com/jeff-lewis/cls-hooked#readme - -Copyright (c) 2013-2016, Forrest L Norvell - -Copyright (c) 2013-2016, Forrest L Norvell -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 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -continuation-local-storage 3.2.1 - BSD-2-Clause -https://github.com/othiym23/node-continuation-local-storage#readme - -Copyright (c) 2013-2016, Forrest L Norvell - -Copyright (c) 2013-2016, Forrest L Norvell -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 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -emitter-listener 1.1.2 - BSD-2-Clause -https://github.com/othiym23/emitter-listener - - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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.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 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 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -shimmer 1.2.1 - BSD-2-Clause -https://github.com/othiym23/shimmer#readme - -Copyright (c) 2013-2019, Forrest L Norvell - -BSD 2-Clause License - -Copyright (c) 2013-2019, Forrest L Norvell -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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/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/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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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/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/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.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/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/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/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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -hoist-non-react-statics 3.3.0 - 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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/ - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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.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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - -==== - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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.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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -@sindresorhus/is 0.7.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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -@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/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/uuid 3.4.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/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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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.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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -applicationinsights 1.7.4 - MIT -https://github.com/Microsoft/ApplicationInsights-node.js#readme - -Copyright (c) Microsoft Corporation. - -The MIT License (MIT) -Copyright © 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-hook-jl 1.7.6 - MIT -https://github.com/jeff-lewis/async-hook-jl#readme - -Copyright (c) 2015 Andreas Madsen - -Copyright (c) 2015 Andreas Madsen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -cacheable-request 2.1.4 - MIT -https://github.com/lukechilds/cacheable-request - -(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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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.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 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.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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -diagnostic-channel-publishers 0.3.4 - 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -future 0.18.2 - MIT - - -Copyright 2006 Google, Inc. -Copyright 2013 by the Jinja team -Copyright (c) 2013 - Damian Avila -Copyright 2008 by Armin Ronacher. -Copyright (c) 2000 Bastian Kleineidam -Copyright (c) 2010 by Armin Ronacher. -Copyright (c) 1999-2002 by Fredrik Lundh. -Copyright (c) 1999-2002 by Secret Labs AB. -Copyright 2013-2019 Python Charmers Pty Ltd -copyright u'2013-2019, Python Charmers Pty Ltd -Copyright (c) 2013-2019 Python Charmers Pty Ltd -Copyright (c) 2001-2006 Python Software Foundation -Copyright (c) 2001-2007 Python Software Foundation -Copyright (c) 2001-2010 Python Software Foundation -Copyright (c) 2002-2006 Python Software Foundation -Copyright (c) 2002-2007 Python Software Foundation -Copyright (c) 2004-2006 Python Software Foundation -Copyright 2000 by Timothy O'Malley -Copyright 2011 by Armin Ronacher. :license Flask Design License -Copyright (c) 2000 Luke Kenneth Casson Leighton -Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Python Software Foundation. - -Copyright (c) 2013-2019 Python Charmers Pty Ltd, Australia - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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) 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -got 8.3.2 - 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -has-symbol-support-x 1.4.2 - MIT -https://github.com/Xotic750/has-symbol-support-x - -Copyright (c) 2015-present -Copyright (c) 2015-present Graham Fairweather. - -https://opensource.org/licenses/MIT - -Copyright (c) 2015-present Graham Fairweather. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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-to-string-tag-x 1.4.1 - MIT -https://github.com/Xotic750/has-to-string-tag-x - -Copyright (c) 2015-2017 -Copyright (c) 2015-2017 Graham Fairweather. - -https://opensource.org/licenses/MIT - -Copyright (c) 2015-2017 Graham Fairweather. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -into-stream 3.1.0 - MIT -https://github.com/sindresorhus/into-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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-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-object 1.0.1 - MIT -https://github.com/ljharb/is-object - -Copyright (c) 2013 Colingo. - -Copyright (c) 2013 Colingo. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -is-plain-obj 1.1.0 - MIT -https://github.com/sindresorhus/is-plain-obj - -(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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -isurl 1.0.0 - MIT -https://github.com/stevenvachon/isurl#readme - -Copyright (c) 2017 Steven Vachon - -MIT License - -Copyright (c) 2017 Steven Vachon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - -= = = = = - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -lodash 4.17.15 - 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.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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -lowercase-keys 1.0.0 - MIT -https://github.com/sindresorhus/lowercase-keys - -(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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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-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-url 2.0.1 - MIT -https://github.com/sindresorhus/normalize-url#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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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-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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -p-cancelable 0.4.1 - 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 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 4.0.1 - 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -p-is-promise 1.1.0 - MIT -https://github.com/sindresorhus/p-is-promise#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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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.** - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 2.0.1 - MIT -https://github.com/sindresorhus/p-timeout#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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -query-string 5.1.1 - MIT -https://github.com/sindresorhus/query-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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.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.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. -""" - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -sort-keys 1.1.2 - MIT -https://github.com/sindresorhus/sort-keys#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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -sort-keys 2.0.0 - MIT -https://github.com/sindresorhus/sort-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-chain 1.3.7 - MIT -https://github.com/AndreasMadsen/stack-chain#readme - -Copyright 2012 the V8 project -Copyright (c) 2012 Andreas Madsen - -Copyright (c) 2012 Andreas Madsen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -strict-uri-encode 1.1.0 - MIT -https://github.com/kevva/strict-uri-encode#readme - -(c) Kevin Martensson (http://github.com/kevva) -Copyright (c) Kevin Martensson - -The MIT License (MIT) - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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.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_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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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[]) - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 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. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -url-to-options 1.0.1 - MIT -https://github.com/stevenvachon/url-to-options#readme - -Copyright (c) 2017 Steven Vachon - -MIT License - -Copyright (c) 2017 Steven Vachon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 8.2.0 - MIT -https://github.com/uuidjs/uuid#readme - -Copyright 2011, Sebastian Tschan https://blueimp.net -Copyright (c) Paul Johnston 1999 - 2009 Other contributors Greg Holt, Andrew Kepert, Ydnar, Lostinet - -The MIT License (MIT) - -Copyright (c) 2010-2020 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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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-extension-telemetry 0.1.4 - MIT -https://github.com/Microsoft/vscode-extension-telemetry#readme - -Copyright (c) Microsoft Corporation. - -vscode-extension-telemetry - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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 - - ---------------------------------------------------------- - ---------------------------------------------------------- - -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. - ---------------------------------------------------------- diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt index 1162db48bc9c..9e7e822af1bb 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -6,20 +6,17 @@ Microsoft Python extension for Visual Studio Code incorporates third party mater 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) +3. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +4. PTVS (https://github.com/Microsoft/PTVS) +5. Python documentation (https://docs.python.org/) +6. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +7. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +8. Sphinx (http://sphinx-doc.org/) +9. nteract (https://github.com/nteract/nteract) +10. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) +11. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) +12. mocha (https://github.com/mochajs/mocha) +13. get-pip (https://github.com/pypa/get-pip) %% Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE BEGIN HERE @@ -246,25 +243,6 @@ 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 @@ -966,107 +944,9 @@ Apache 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 +vscode-cpptools Copyright (c) Microsoft Corporation @@ -1074,97 +954,81 @@ 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, +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the Software), to deal in the +Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION 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. +%% mocha NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +(The MIT License) -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +Copyright (c) 2011-2020 OpenJS Foundation and contributors, https://openjsf.org -* 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. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -* 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. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 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 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 -======= +END OF mocha NOTICES, INFORMATION, AND LICENSE + -%% font-awesome NOTICES, INFORMATION, AND LICENSE BEGIN HERE +%% get-pip 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.** + +Copyright (c) 2008-2019 The pip developers + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +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 font-awesome NOTICES, INFORMATION, AND LICENSE \ No newline at end of file +END OF get-pip NOTICES, INFORMATION, AND LICENSE diff --git a/build/.eslintrc b/build/.eslintrc deleted file mode 100644 index 8a44c2cfe2cc..000000000000 --- a/build/.eslintrc +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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 index 539aa1c15b60..abe46f117f5b 100644 --- a/build/.mocha-multi-reporters.config +++ b/build/.mocha-multi-reporters.config @@ -1,3 +1,3 @@ { - "reporterEnabled": "spec,mocha-junit-reporter" + "reporterEnabled": "./build/ci/scripts/spec_with_pid,mocha-junit-reporter" } diff --git a/build/.mocha.functional.json b/build/.mocha.functional.json index 3cebb9367a52..71998902e984 100644 --- a/build/.mocha.functional.json +++ b/build/.mocha.functional.json @@ -1,9 +1,11 @@ { "spec": "./out/test/**/*.functional.test.js", - "require": ["out/test/unittests.js"], + "require": [ + "out/test/unittests.js" + ], "exclude": "out/**/*.jsx", "reporter": "mocha-multi-reporters", - "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "reporter-option": "configFile=./build/.mocha-multi-reporters.config", "ui": "tdd", "recursive": true, "colors": true, diff --git a/build/.mocha.performance.json b/build/.mocha.performance.json index 4e398d556887..84dc3952cc85 100644 --- a/build/.mocha.performance.json +++ b/build/.mocha.performance.json @@ -7,6 +7,5 @@ "recursive": true, "colors": true, "exit": true, - "timeout": 30000, - "grep": "DataScience" + "timeout": 30000 } diff --git a/build/.mocha.unittests.json b/build/.mocha.unittests.json index b4dcc393c7eb..cb6bff959497 100644 --- a/build/.mocha.unittests.json +++ b/build/.mocha.unittests.json @@ -1,9 +1,13 @@ { "spec": "./out/test/**/*.unit.test.js", - "require": ["out/test/unittests.js"], + "require": [ + "out/test/unittests.js" + ], + "exclude": "out/**/*.jsx", "reporter": "mocha-multi-reporters", - "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "reporter-option": "configFile=./build/.mocha-multi-reporters.config", "ui": "tdd", "recursive": true, - "colors": true + "colors": true, + "timeout": 180000 } diff --git a/build/.nycrc b/build/.nycrc index e9540ef130f2..b92a4f36785d 100644 --- a/build/.nycrc +++ b/build/.nycrc @@ -2,8 +2,8 @@ "extends": "@istanbuljs/nyc-config-typescript", "all": true, "include": [ - "src/client/**/*.ts", "src/test/**/*.js", - "src/datascience-ui/**/*.ts", "src/datascience-ui/**/*.js" + "src/client/**/*.ts", "out/client/**/*.js" ], - "exclude": ["src/test/**/*.ts", "src/test/**/*.js"] -} \ No newline at end of file + "exclude": ["src/test/**/*.ts", "out/test/**/*.js"], + "exclude-node-modules": true +} diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml new file mode 100644 index 000000000000..e7159618d3ae --- /dev/null +++ b/build/azure-pipeline.pre-release.yml @@ -0,0 +1,158 @@ +# Run on a schedule +trigger: none +pr: none + +schedules: + - cron: '0 10 * * 1-5' # 10AM UTC (2AM PDT) MON-FRI (VS Code Pre-release builds at 9PM PDT) + displayName: Nightly Pre-Release Schedule + always: false # only run if there are source code changes + branches: + include: + - main + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + +extends: + template: azure-pipelines/extension/pre-release.yml@templates + parameters: + publishExtension: ${{ parameters.publishExtension }} + ghCreateTag: false + standardizedVersioning: true + l10nSourcePaths: ./src/client + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + + buildSteps: + - task: NodeTool@0 + inputs: + versionSpec: '22.17.0' + displayName: Select Node version + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: python -m pip install -U pip + displayName: Upgrade pip + + - script: python -m pip install wheel nox + displayName: Install wheel and nox + + - script: npm ci + displayName: Install NPM dependencies + + - script: nox --session install_python_libs + displayName: Install Jedi, get-pip, etc + + - script: python ./build/update_package_file.py + displayName: Update telemetry in package.json + + - script: npm run addExtensionPackDependencies + displayName: Update optional extension dependencies + + - script: npx gulp prePublishBundle + displayName: Build + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 591 + buildVersionToDownload: 'latest' + branchName: 'refs/heads/main' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: python -c "import shutil; shutil.rmtree('.nox', ignore_errors=True)" + displayName: Clean up Nox + + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml new file mode 100644 index 000000000000..cd66613eec8d --- /dev/null +++ b/build/azure-pipeline.stable.yml @@ -0,0 +1,153 @@ +trigger: none +# branches: +# include: +# - release* +# tags: +# include: ['*'] +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + +extends: + template: azure-pipelines/extension/stable.yml@templates + parameters: + publishExtension: ${{ parameters.publishExtension }} + l10nSourcePaths: ./src/client + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + + buildSteps: + - task: NodeTool@0 + inputs: + versionSpec: '22.17.0' + displayName: Select Node version + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: python -m pip install -U pip + displayName: Upgrade pip + + - script: python -m pip install wheel nox + displayName: Install wheel and nox + + - script: npm ci + displayName: Install NPM dependencies + + - script: nox --session install_python_libs + displayName: Install Jedi, get-pip, etc + + - script: python ./build/update_package_file.py + displayName: Update telemetry in package.json + + - script: npm run addExtensionPackDependencies + displayName: Update optional extension dependencies + + - script: npx gulp prePublishBundle + displayName: Build + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 593 + buildVersionToDownload: 'latestFromBranch' + branchName: 'refs/heads/release/2026.4' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: python -c "import shutil; shutil.rmtree('.nox', ignore_errors=True)" + displayName: Clean up Nox + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true + apiScanDependentPipelineId: '593' # python-environment-tools + apiScanSoftwareVersion: '2024' diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml new file mode 100644 index 000000000000..0796e38ca598 --- /dev/null +++ b/build/azure-pipelines/pipeline.yml @@ -0,0 +1,58 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +name: $(Date:yyyyMMdd)$(Rev:.r) + +trigger: none + +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: quality + displayName: Quality + type: string + default: latest + values: + - latest + - next + - name: publishPythonApi + displayName: 🚀 Publish pythonExtensionApi + type: boolean + default: false + +extends: + template: azure-pipelines/npm-package/pipeline.yml@templates + parameters: + npmPackages: + - name: pythonExtensionApi + testPlatforms: + - name: Linux + nodeVersions: + - 22.21.1 + - name: MacOS + nodeVersions: + - 22.21.1 + - name: Windows + nodeVersions: + - 22.21.1 + testSteps: + - template: /build/azure-pipelines/templates/test-steps.yml@self + parameters: + package: pythonExtensionApi + buildSteps: + - template: /build/azure-pipelines/templates/pack-steps.yml@self + parameters: + package: pythonExtensionApi + ghTagPrefix: release/pythonExtensionApi/ + tag: ${{ parameters.quality }} + publishPackage: ${{ parameters.publishPythonApi }} + workingDirectory: $(Build.SourcesDirectory)/pythonExtensionApi diff --git a/build/azure-pipelines/templates/pack-steps.yml b/build/azure-pipelines/templates/pack-steps.yml new file mode 100644 index 000000000000..97037efb59ba --- /dev/null +++ b/build/azure-pipelines/templates/pack-steps.yml @@ -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. +############################################################################################### +parameters: +- name: package + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - script: npm install + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Install package dependencies diff --git a/build/azure-pipelines/templates/test-steps.yml b/build/azure-pipelines/templates/test-steps.yml new file mode 100644 index 000000000000..15eb3db6384d --- /dev/null +++ b/build/azure-pipelines/templates/test-steps.yml @@ -0,0 +1,23 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + type: string +- name: script + type: string + default: 'all:publish' + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - bash: | + /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + echo ">>> Started xvfb" + displayName: Start xvfb + condition: eq(variables['Agent.OS'], 'Linux') + - script: npm run ${{ parameters.script }} + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Verify package diff --git a/build/build-install-requirements.txt b/build/build-install-requirements.txt new file mode 100644 index 000000000000..8baaa59ded67 --- /dev/null +++ b/build/build-install-requirements.txt @@ -0,0 +1,2 @@ +# Requirements needed to run install_debugpy.py and download_get_pip.py +packaging diff --git a/build/ci/TSAOptions.json b/build/ci/TSAOptions.json deleted file mode 100644 index 8edad9f01b84..000000000000 --- a/build/ci/TSAOptions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "projectName": "PVSC", - "areaPath": "PVSC\\Security", - "iterationPath": "PVSC", - "allTools": true -} diff --git a/build/ci/addEnvPath.py b/build/ci/addEnvPath.py index abad9ec3b5c9..66eff2a7b25d 100644 --- a/build/ci/addEnvPath.py +++ b/build/ci/addEnvPath.py @@ -3,7 +3,8 @@ #Adds the virtual environment's executable path to json file -import json,sys +import json +import sys import os.path jsonPath = sys.argv[1] key = sys.argv[2] diff --git a/build/ci/codecov.yml b/build/ci/codecov.yml deleted file mode 100644 index 8ed52955f093..000000000000 --- a/build/ci/codecov.yml +++ /dev/null @@ -1,27 +0,0 @@ -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 index 0467c61b793d..a1b589e38a32 100644 --- a/build/ci/conda_base.yml +++ b/build/ci/conda_base.yml @@ -1,5 +1 @@ -pandas -jupyter -numpy -matplotlib pip diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml index 8d5ef6398197..4f9ceefd27fb 100644 --- a/build/ci/conda_env_1.yml +++ b/build/ci/conda_env_1.yml @@ -1,8 +1,4 @@ name: conda_env_1 dependencies: - - python=3.7 - - pandas - - jupyter - - numpy - - matplotlib + - python=3.9 - pip diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml index 6ccdb55b80b7..af9d7a46ba3e 100644 --- a/build/ci/conda_env_2.yml +++ b/build/ci/conda_env_2.yml @@ -1,8 +1,4 @@ name: conda_env_2 dependencies: - - python=3.8 - - pandas - - jupyter - - numpy - - matplotlib + - python=3.9 - pip diff --git a/build/ci/performance/DS_test_benchmark.json b/build/ci/performance/DS_test_benchmark.json deleted file mode 100644 index 5d83e811700a..000000000000 --- a/build/ci/performance/DS_test_benchmark.json +++ /dev/null @@ -1,542 +0,0 @@ -[ - { - "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 deleted file mode 100644 index f632fd0b5224..000000000000 --- a/build/ci/performance/checkPerformanceResults.js +++ /dev/null @@ -1,94 +0,0 @@ -// 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 deleted file mode 100644 index 7cdf93d3ac81..000000000000 --- a/build/ci/performance/createNewPerformanceBenchmark.js +++ /dev/null @@ -1,55 +0,0 @@ -// 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 deleted file mode 100644 index f902ff0c5f9d..000000000000 --- a/build/ci/performance/perfJobs.yaml +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 456eb254331b..000000000000 --- a/build/ci/performance/savePerformanceResults.js +++ /dev/null @@ -1,79 +0,0 @@ -// 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 deleted file mode 100644 index 3e84f35805bf..000000000000 --- a/build/ci/performance/vscode-python-performance.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: '$(Year:yyyy).$(Month).0.$(BuildID)-weekly-performance-test' - -trigger: none -pr: none -schedules: - - cron: '0 0 * * sat' - displayName: Weekly Performance Test - branches: - include: - - master - -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 deleted file mode 100644 index be42594e0504..000000000000 --- a/build/ci/postInstall.js +++ /dev/null @@ -1,78 +0,0 @@ -// 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/pyproject.toml b/build/ci/pyproject.toml new file mode 100644 index 000000000000..6335f021a637 --- /dev/null +++ b/build/ci/pyproject.toml @@ -0,0 +1,8 @@ +[tool.poetry] +name = "poetry-tutorial-project" +version = "0.1.0" +description = "" +authors = [""] + +[tool.poetry.dependencies] +python = "*" diff --git a/build/ci/scripts/spec_with_pid.js b/build/ci/scripts/spec_with_pid.js new file mode 100644 index 000000000000..a8453353aa79 --- /dev/null +++ b/build/ci/scripts/spec_with_pid.js @@ -0,0 +1,103 @@ +'use strict'; + +/** + * @module Spec + */ +/** + * Module dependencies. + */ + +const Base = require('mocha/lib/reporters/base'); +const { constants } = require('mocha/lib/runner'); + +const { EVENT_RUN_BEGIN } = constants; +const { EVENT_RUN_END } = constants; +const { EVENT_SUITE_BEGIN } = constants; +const { EVENT_SUITE_END } = constants; +const { EVENT_TEST_FAIL } = constants; +const { EVENT_TEST_PASS } = constants; +const { EVENT_TEST_PENDING } = constants; +const { inherits } = require('mocha/lib/utils'); + +const { color } = Base; + +const prefix = process.env.VSC_PYTHON_CI_TEST_PARALLEL ? `${process.pid} ` : ''; + +/** + * Constructs a new `Spec` reporter instance. + * + * @public + * @class + * @memberof Mocha.reporters + * @extends Mocha.reporters.Base + * @param {Runner} runner - Instance triggers reporter actions. + * @param {Object} [options] - runner options + */ +function Spec(runner, options) { + Base.call(this, runner, options); + + let indents = 0; + let n = 0; + + function indent() { + return Array(indents).join(' '); + } + + runner.on(EVENT_RUN_BEGIN, () => { + Base.consoleLog(); + }); + + runner.on(EVENT_SUITE_BEGIN, (suite) => { + indents += 1; + Base.consoleLog(color('suite', `${prefix}%s%s`), indent(), suite.title); + }); + + runner.on(EVENT_SUITE_END, () => { + indents -= 1; + if (indents === 1) { + Base.consoleLog(); + } + }); + + runner.on(EVENT_TEST_PENDING, (test) => { + const fmt = indent() + color('pending', `${prefix} %s`); + Base.consoleLog(fmt, test.title); + }); + + runner.on(EVENT_TEST_PASS, (test) => { + let fmt; + if (test.speed === 'fast') { + fmt = indent() + color('checkmark', prefix + Base.symbols.ok) + color('pass', ' %s'); + Base.consoleLog(fmt, test.title); + } else { + fmt = + indent() + + color('checkmark', prefix + Base.symbols.ok) + + color('pass', ' %s') + + color(test.speed, ' (%dms)'); + Base.consoleLog(fmt, test.title, test.duration); + } + }); + + runner.on(EVENT_TEST_FAIL, (test) => { + n += 1; + Base.consoleLog(indent() + color('fail', `${prefix}%d) %s`), n, test.title); + }); + + runner.once(EVENT_RUN_END, this.epilogue.bind(this)); +} + +/** + * Inherit from `Base.prototype`. + */ +inherits(Spec, Base); + +Spec.description = 'hierarchical & verbose [default]'; + +/** + * Expose `Spec`. + */ + +// eslint-disable-next-line no-global-assign +exports = Spec; +module.exports = exports; diff --git a/build/ci/static_analysis/policheck/exceptions.mdb b/build/ci/static_analysis/policheck/exceptions.mdb new file mode 100644 index 000000000000..d4a413f897e1 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 deleted file mode 100644 index 03457023e99e..000000000000 --- a/build/ci/templates/globals.yml +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 13b6915f7e3d..000000000000 --- a/build/ci/templates/jobs/build_compile.yml +++ /dev/null @@ -1,50 +0,0 @@ -# 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 deleted file mode 100644 index 50fb4af733e0..000000000000 --- a/build/ci/templates/jobs/coverage.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 2d7b34f4d1d7..000000000000 --- a/build/ci/templates/steps/build.yml +++ /dev/null @@ -1,59 +0,0 @@ -# ----------------------------------------------------------------------------------------------------------------------------- -# 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' - condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master')) - - - 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 deleted file mode 100644 index e35db11613a8..000000000000 --- a/build/ci/templates/steps/compile.yml +++ /dev/null @@ -1,14 +0,0 @@ -# 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 deleted file mode 100644 index 5850b0def046..000000000000 --- a/build/ci/templates/steps/dependencies.yml +++ /dev/null @@ -1,20 +0,0 @@ -# ----------------------------------------------------------------------------------------------------------------------------- -# 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 deleted file mode 100644 index 608a2fff8bdc..000000000000 --- a/build/ci/templates/steps/generate_upload_coverage.yml +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index ed6c6b0c4fb4..000000000000 --- a/build/ci/templates/steps/initialization.yml +++ /dev/null @@ -1,135 +0,0 @@ -# ----------------------------------------------------------------------------------------------------------------------------- -# 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 deleted file mode 100644 index 51ef76aac61c..000000000000 --- a/build/ci/templates/steps/merge_upload_coverage.yml +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 3dc15405474a..000000000000 --- a/build/ci/templates/test_phases.yml +++ /dev/null @@ -1,540 +0,0 @@ -# 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. - # - # This task only runs if the string 'testFunctional' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run test:functional - - script: | - npm run test:functional - displayName: 'Run functional tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testFunctional')) - 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: '$(MOCHA_FILE)' - 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 deleted file mode 100644 index bad74182082c..000000000000 --- a/build/ci/vscode-python-ci-manual.yaml +++ /dev/null @@ -1,296 +0,0 @@ -# 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 deleted file mode 100644 index 6409755854e4..000000000000 --- a/build/ci/vscode-python-ci-static-analysis.yaml +++ /dev/null @@ -1,89 +0,0 @@ -# CI build (PR merge) - -name: 'VSCode-Python-ci-static-analysis' - -# Notes: Only trigger a commit for master and release, and skip build/rebuild -# on changes in the news and .vscode folders. -trigger: - branches: - include: ['master', 'release*'] - paths: - exclude: ['/news/1 Enhancements', '/news/2 Fixes', '/news/3 Code Health', '/.vscode'] - -# Not the PR build for merges to master 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 deleted file mode 100644 index 5ebd33761a14..000000000000 --- a/build/ci/vscode-python-ci.yaml +++ /dev/null @@ -1,194 +0,0 @@ -# CI build (PR merge) - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-ci' - -# Notes: Only trigger a commit for master and release, and skip build/rebuild -# on changes in the news and .vscode folders. -trigger: - branches: - include: ['master', 'release*'] - paths: - exclude: ['/news/1 Enhancements', '/news/2 Fixes', '/news/3 Code Health', '/.vscode'] - -# Not the PR build for merges to master 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 deleted file mode 100644 index 7be04e88561c..000000000000 --- a/build/ci/vscode-python-nightly-ci.yaml +++ /dev/null @@ -1,510 +0,0 @@ -# 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 master and release. -pr: none - -schedules: - - cron: '0 8 * * 1-5' - # Daily midnight PST build, runs Monday - Friday always - displayName: Nightly build - branches: - include: - - master - - 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 deleted file mode 100644 index 720faa3d2fb5..000000000000 --- a/build/ci/vscode-python-nightly-flake-ci.yaml +++ /dev/null @@ -1,89 +0,0 @@ -# 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 master and release. -pr: none - -schedules: - - cron: '0 8 * * 1-5' - # Daily midnight PST build, runs Monday - Friday always - displayName: Nightly Flake build - branches: - include: - - master - - 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: 120 - strategy: - matrix: - 'Functional': - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: 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 deleted file mode 100644 index f46d58cbef18..000000000000 --- a/build/ci/vscode-python-pr-validation.yaml +++ /dev/null @@ -1,133 +0,0 @@ -# PR Validation build. - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-pr' - -# Notes: Only trigger a PR build for master and release, and skip build/rebuild -# on changes in the news and .vscode folders. -pr: - autoCancel: true - branches: - include: - - 'master' - - '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 master 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 - '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 deleted file mode 100644 index a00a76726786..000000000000 --- a/build/conda-functional-requirements.txt +++ /dev/null @@ -1,26 +0,0 @@ -# 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 index 60109ea4ce0e..73815ebea45f 100644 --- a/build/constants.js +++ b/build/constants.js @@ -1,8 +1,10 @@ // 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'); diff --git a/build/debug/replaceWithWebBrowserPanel.js b/build/debug/replaceWithWebBrowserPanel.js deleted file mode 100644 index 21c293117535..000000000000 --- a/build/debug/replaceWithWebBrowserPanel.js +++ /dev/null @@ -1,26 +0,0 @@ -// 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 deleted file mode 100644 index 6ee0765db4b3..000000000000 --- a/build/debugger-install-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Requirements needed to run install_debugpy.py -packaging diff --git a/build/existingFiles.json b/build/existingFiles.json index 4dbf1af34b78..48ab84ff565d 100644 --- a/build/existingFiles.json +++ b/build/existingFiles.json @@ -170,7 +170,6 @@ "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", @@ -196,7 +195,6 @@ "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", @@ -381,6 +379,7 @@ "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.nushell.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", @@ -501,7 +500,7 @@ "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/python_files/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", diff --git a/build/fail.js b/build/fail.js new file mode 100644 index 000000000000..2adc808d8da9 --- /dev/null +++ b/build/fail.js @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +process.exitCode = 1; diff --git a/build/functional-test-requirements.txt b/build/functional-test-requirements.txt index d2f1977a7be4..5c3a9e3116ed 100644 --- a/build/functional-test-requirements.txt +++ b/build/functional-test-requirements.txt @@ -1,2 +1,5 @@ # List of requirements for functional tests versioneer +numpy +pytest +pytest-cov diff --git a/build/ipython-test-requirements.txt b/build/ipython-test-requirements.txt deleted file mode 100644 index 688e039a4461..000000000000 --- a/build/ipython-test-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -# List of requirements for ipython tests -numpy -pandas -ipython diff --git a/build/license-header.txt b/build/license-header.txt new file mode 100644 index 000000000000..2970b03d7a1c --- /dev/null +++ b/build/license-header.txt @@ -0,0 +1,9 @@ +PLEASE NOTE: This is the license for the Python extension for Visual Studio Code. The Python extension automatically installs other extensions as optional dependencies, which can be uninstalled at any time. These extensions have separate licenses: + + - The Python Debugger extension is released under an MIT License: + https://marketplace.visualstudio.com/items/ms-python.debugpy/license + + - The Pylance extension is only available in binary form and is released under a Microsoft proprietary license, the terms of which are available here: + https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license + +------------------------------------------------------------------------------ diff --git a/build/test-requirements.txt b/build/test-requirements.txt index 49c1dd2573a2..6d64ff72ac7f 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -1,19 +1,42 @@ -# Install flake8 first, as both flake8 and autopep8 require pycodestyle, -# but flake8 has a tighter pinning. +# pin setoptconf to prevent issue with 'use_2to3' +setoptconf==0.3.0 + 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==4.6.9 # Last version of pytest with Python 2.7 support -py==1.8.1 -rope +prospector +pytest flask +fastapi +uvicorn django -isort -pathlib2>=2.2.0 ; python_version<'3.6' # Python 2.7 compatibility (pytest) +testresources +testscenarios + +# Integrated TensorBoard tests +tensorboard +torch-tb-profiler + +# extension build tests +freezegun + +# testing custom pytest plugin require the use of named pipes +namedpipe; platform_system == "Windows" + +# typing for Django files +django-stubs + +coverage +pytest-cov +pytest-json +pytest-timeout + + +# for pytest-describe related tests +pytest-describe + +# for pytest-ruff related tests +pytest-ruff +pytest-black diff --git a/build/test_update_ext_version.py b/build/test_update_ext_version.py new file mode 100644 index 000000000000..b94484775f59 --- /dev/null +++ b/build/test_update_ext_version.py @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import datetime +import json + +import freezegun +import pytest +import update_ext_version + + +CURRENT_YEAR = datetime.datetime.now().year +TEST_DATETIME = f"{CURRENT_YEAR}-03-14 01:23:45" + +# The build ID is calculated via: +# "1" + datetime.datetime.strptime(TEST_DATETIME,"%Y-%m-%d %H:%M:%S").strftime('%j%H%M') +EXPECTED_BUILD_ID = "10730123" + + +def create_package_json(directory, version): + """Create `package.json` in `directory` with a specified version of `version`.""" + package_json = directory / "package.json" + package_json.write_text(json.dumps({"version": version}), encoding="utf-8") + return package_json + + +def run_test(tmp_path, version, args, expected): + package_json = create_package_json(tmp_path, version) + update_ext_version.main(package_json, args) + package = json.loads(package_json.read_text(encoding="utf-8")) + assert expected == update_ext_version.parse_version(package["version"]) + + +@pytest.mark.parametrize( + "version, args", + [ + ("2000.1.0", []), # Wrong year for CalVer + (f"{CURRENT_YEAR}.0.0-rc", []), + (f"{CURRENT_YEAR}.1.0-rc", ["--release"]), + (f"{CURRENT_YEAR}.0.0-rc", ["--release", "--build-id", "-1"]), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--for-publishing", "--build-id", "-1"], + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--for-publishing", "--build-id", "999999999999"], + ), + (f"{CURRENT_YEAR}.1.0-rc", ["--build-id", "-1"]), + (f"{CURRENT_YEAR}.1.0-rc", ["--for-publishing", "--build-id", "-1"]), + (f"{CURRENT_YEAR}.1.0-rc", ["--for-publishing", "--build-id", "999999999999"]), + ], +) +def test_invalid_args(tmp_path, version, args): + with pytest.raises(ValueError): + run_test(tmp_path, version, args, None) + + +@pytest.mark.parametrize( + "version, args, expected", + [ + ( + f"{CURRENT_YEAR}.1.0-rc", + ["--build-id", "12345"], + (f"{CURRENT_YEAR}", "1", "12345", "rc"), + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--build-id", "12345"], + (f"{CURRENT_YEAR}", "0", "12345", ""), + ), + ( + f"{CURRENT_YEAR}.1.0-rc", + ["--for-publishing", "--build-id", "12345"], + (f"{CURRENT_YEAR}", "1", "12345", ""), + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--for-publishing", "--build-id", "12345"], + (f"{CURRENT_YEAR}", "0", "12345", ""), + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--build-id", "999999999999"], + (f"{CURRENT_YEAR}", "0", "999999999999", ""), + ), + ( + f"{CURRENT_YEAR}.1.0-rc", + ["--build-id", "999999999999"], + (f"{CURRENT_YEAR}", "1", "999999999999", "rc"), + ), + ( + f"{CURRENT_YEAR}.1.0-rc", + [], + (f"{CURRENT_YEAR}", "1", EXPECTED_BUILD_ID, "rc"), + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release"], + (f"{CURRENT_YEAR}", "0", "0", ""), + ), + ( + f"{CURRENT_YEAR}.1.0-rc", + ["--for-publishing"], + (f"{CURRENT_YEAR}", "1", EXPECTED_BUILD_ID, ""), + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--for-publishing"], + (f"{CURRENT_YEAR}", "0", "0", ""), + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release"], + (f"{CURRENT_YEAR}", "0", "0", ""), + ), + ( + f"{CURRENT_YEAR}.1.0-rc", + [], + (f"{CURRENT_YEAR}", "1", EXPECTED_BUILD_ID, "rc"), + ), + ], +) +@freezegun.freeze_time(f"{CURRENT_YEAR}-03-14 01:23:45") +def test_update_ext_version(tmp_path, version, args, expected): + run_test(tmp_path, version, args, expected) diff --git a/build/tslint-rules/baseRuleWalker.js b/build/tslint-rules/baseRuleWalker.js deleted file mode 100644 index b8ce93d4179d..000000000000 --- a/build/tslint-rules/baseRuleWalker.js +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index acf4beaba811..000000000000 --- a/build/tslint-rules/messagesMustBeLocalizedRule.js +++ /dev/null @@ -1,71 +0,0 @@ -// 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/update_ext_version.py b/build/update_ext_version.py new file mode 100644 index 000000000000..6d709ae05f7f --- /dev/null +++ b/build/update_ext_version.py @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import datetime +import json +import pathlib +import sys +from typing import Sequence, Tuple, Union + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + + +def build_arg_parse() -> argparse.ArgumentParser: + """Builds the arguments parser.""" + parser = argparse.ArgumentParser( + description="This script updates the python extension micro version based on the release or pre-release channel." + ) + parser.add_argument( + "--release", + action="store_true", + help="Treats the current build as a release build.", + ) + parser.add_argument( + "--build-id", + action="store", + type=int, + default=None, + help="If present, will be used as a micro version.", + required=False, + ) + parser.add_argument( + "--for-publishing", + action="store_true", + help="Removes `-dev` or `-rc` suffix.", + ) + return parser + + +def is_even(v: Union[int, str]) -> bool: + """Returns True if `v` is even.""" + return not int(v) % 2 + + +def micro_build_number() -> str: + """Generates the micro build number. + The format is `1`. + """ + return f"1{datetime.datetime.now(tz=datetime.timezone.utc).strftime('%j%H%M')}" + + +def parse_version(version: str) -> Tuple[str, str, str, str]: + """Parse a version string into a tuple of version parts.""" + major, minor, parts = version.split(".", maxsplit=2) + try: + micro, suffix = parts.split("-", maxsplit=1) + except ValueError: + micro = parts + suffix = "" + return major, minor, micro, suffix + + +def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: + parser = build_arg_parse() + args = parser.parse_args(argv) + + package = json.loads(package_json.read_text(encoding="utf-8")) + + major, minor, micro, suffix = parse_version(package["version"]) + + current_year = datetime.datetime.now().year + current_month = datetime.datetime.now().month + int_major = int(major) + valid_major = ( + int_major + == current_year # Between JAN-DEC major version should be current year + or ( + int_major == current_year - 1 and current_month == 1 + ) # After new years the check is relaxed for JAN to allow releases of previous year DEC + or ( + int_major == current_year + 1 and current_month == 12 + ) # Before new years the check is relaxed for DEC to allow pre-releases of next year JAN + ) + if not valid_major: + raise ValueError( + f"Major version [{major}] must be the current year [{current_year}].", + f"If changing major version after new year's, change to {current_year}.1.0", + "Minor version must be updated based on release or pre-release channel.", + ) + + if args.release and not is_even(minor): + raise ValueError( + f"Release version should have EVEN numbered minor version: {package['version']}" + ) + elif not args.release and is_even(minor): + raise ValueError( + f"Pre-Release version should have ODD numbered minor version: {package['version']}" + ) + + print(f"Updating build FROM: {package['version']}") + if args.build_id: + # If build id is provided it should fall within the 0-INT32 max range + # that the max allowed value for publishing to the Marketplace. + if args.build_id < 0 or (args.for_publishing and args.build_id > ((2**32) - 1)): + raise ValueError(f"Build ID must be within [0, {(2**32) - 1}]") + + package["version"] = ".".join((major, minor, str(args.build_id))) + elif args.release: + package["version"] = ".".join((major, minor, micro)) + else: + # micro version only updated for pre-release. + package["version"] = ".".join((major, minor, micro_build_number())) + + if not args.for_publishing and not args.release and len(suffix): + package["version"] += "-" + suffix + print(f"Updating build TO: {package['version']}") + + # Overwrite package.json with new data add a new-line at the end of the file. + package_json.write_text( + json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + +if __name__ == "__main__": + main(PACKAGE_JSON_PATH, sys.argv[1:]) diff --git a/build/update_package_file.py b/build/update_package_file.py new file mode 100644 index 000000000000..f82587ced846 --- /dev/null +++ b/build/update_package_file.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + + +def main(package_json: pathlib.Path) -> None: + package = json.loads(package_json.read_text(encoding="utf-8")) + package["enableTelemetry"] = True + + # Overwrite package.json with new data add a new-line at the end of the file. + package_json.write_text( + json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + +if __name__ == "__main__": + main(PACKAGE_JSON_PATH) diff --git a/build/util.js b/build/util.js index 93a60e7fb19b..c54e204ae7d7 100644 --- a/build/util.js +++ b/build/util.js @@ -1,9 +1,11 @@ // 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); @@ -12,8 +14,6 @@ function getListOfFiles(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)); - }); + return files.map((file) => path.join(exports.ExtensionRootDir, file.replace(/\//g, path.sep))); } exports.getListOfFiles = getListOfFiles; diff --git a/build/webpack/common.js b/build/webpack/common.js index 4d3ef5a2367a..c7f7460adf86 100644 --- a/build/webpack/common.js +++ b/build/webpack/common.js @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; const glob = require('glob'); const path = require('path'); +// eslint-disable-next-line camelcase const webpack_bundle_analyzer = require('webpack-bundle-analyzer'); const constants = require('../constants'); + exports.nodeModulesToExternalize = [ 'unicode/category/Lu', 'unicode/category/Ll', @@ -17,22 +20,10 @@ exports.nodeModulesToExternalize = [ '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) { @@ -45,8 +36,8 @@ function getDefaultPlugins(name) { reportFilename: `${name}.analyzer.html`, generateStatsFile: true, statsFilename: `${name}.stats.json`, - openAnalyzer: false // Open file manually if you want to see it :) - }) + openAnalyzer: false, // Open file manually if you want to see it :) + }), ); } return plugins; diff --git a/build/webpack/loaders/externalizeDependencies.js b/build/webpack/loaders/externalizeDependencies.js index ff2fc0b81ea2..0ada9b0424d8 100644 --- a/build/webpack/loaders/externalizeDependencies.js +++ b/build/webpack/loaders/externalizeDependencies.js @@ -2,12 +2,14 @@ // 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 + +// eslint-disable-next-line camelcase function default_1(source) { common.nodeModulesToReplacePaths.forEach((moduleName) => { if (source.indexOf(moduleName) > 0) { @@ -21,4 +23,5 @@ function default_1(source) { }); return source; } +// eslint-disable-next-line camelcase exports.default = default_1; diff --git a/build/webpack/loaders/fixNodeFetch.js b/build/webpack/loaders/fixNodeFetch.js deleted file mode 100644 index 0d648d131349..000000000000 --- a/build/webpack/loaders/fixNodeFetch.js +++ /dev/null @@ -1,36 +0,0 @@ -// 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/pdfkit.js b/build/webpack/pdfkit.js deleted file mode 100644 index 5c31590a3924..000000000000 --- a/build/webpack/pdfkit.js +++ /dev/null @@ -1,15 +0,0 @@ -// 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 deleted file mode 100644 index 7c42013b4e55..000000000000 --- a/build/webpack/plugins/less-plugin-base64.js +++ /dev/null @@ -1,64 +0,0 @@ -// 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 deleted file mode 100644 index e661d5ee7620..000000000000 --- a/build/webpack/webpack.datascience-ui-notebooks.config.js +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 022a483c113a..000000000000 --- a/build/webpack/webpack.datascience-ui-renderers.config.js +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 89e3f2c56fdd..000000000000 --- a/build/webpack/webpack.datascience-ui-viewers.config.js +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index c85046241aa6..000000000000 --- a/build/webpack/webpack.datascience-ui.config.builder.js +++ /dev/null @@ -1,339 +0,0 @@ -// 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 deleted file mode 100644 index 5edb7cf8166a..000000000000 --- a/build/webpack/webpack.datascience-ui.config.js +++ /dev/null @@ -1,7 +0,0 @@ -// 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.browser.config.js b/build/webpack/webpack.extension.browser.config.js new file mode 100644 index 000000000000..909cceaf1bea --- /dev/null +++ b/build/webpack/webpack.extension.browser.config.js @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// @ts-check + +'use strict'; + +const path = require('path'); +const webpack = require('webpack'); +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); + +const packageRoot = path.resolve(__dirname, '..', '..'); +const outDir = path.resolve(packageRoot, 'dist'); + +/** @type {(env: any, argv: { mode: 'production' | 'development' | 'none' }) => import('webpack').Configuration} */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const nodeConfig = (_, { mode }) => ({ + context: packageRoot, + entry: { + extension: './src/client/browser/extension.ts', + }, + target: 'webworker', + output: { + filename: '[name].browser.js', + path: outDir, + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../../[resource-path]', + }, + devtool: 'source-map', + // stats: { + // all: false, + // errors: true, + // warnings: true, + // }, + resolve: { + extensions: ['.ts', '.js'], + fallback: { path: require.resolve('path-browserify') }, + }, + plugins: [ + new NodePolyfillPlugin(), + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + externals: { + vscode: 'commonjs vscode', + + // These dependencies are ignored because we don't use them, and App Insights has try-catch protecting their loading if they don't exist + // See: https://github.com/microsoft/vscode-extension-telemetry/issues/41#issuecomment-598852991 + 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', + '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', + }, + module: { + rules: [ + { + test: /\.ts$/, + loader: 'ts-loader', + options: { + configFile: 'tsconfig.browser.json', + }, + }, + { + test: /\.node$/, + loader: 'node-loader', + }, + ], + }, + // optimization: { + // usedExports: true, + // splitChunks: { + // cacheGroups: { + // defaultVendors: { + // name: 'vendor', + // test: /[\\/]node_modules[\\/]/, + // chunks: 'all', + // priority: -10, + // }, + // }, + // }, + // }, +}); + +module.exports = nodeConfig; diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index 8e10d6a34317..082ce52a4d32 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -1,95 +1,90 @@ // 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'); +// eslint-disable-next-line camelcase 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). +// 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' + extension: './src/client/extension.ts', + 'shellExec.worker': './src/client/common/process/worker/shellExec.worker.ts', + 'plainExec.worker': './src/client/common/process/worker/plainExec.worker.ts', + 'registryKeys.worker': 'src/client/pythonEnvironments/common/registryKeys.worker.ts', + 'registryValues.worker': 'src/client/pythonEnvironments/common/registryValues.worker.ts', }, devtool: 'source-map', node: { - __dirname: false + __dirname: false, }, module: { rules: [ { - // JupyterServices imports node-fetch. - test: /@jupyterlab[\\\/]services[\\\/].*js$/, + test: /\.ts$/, use: [ { - loader: path.join(__dirname, 'loaders', 'fixNodeFetch.js') - } - ] + loader: path.join(__dirname, 'loaders', 'externalizeDependencies.js'), + }, + ], }, { test: /\.ts$/, + exclude: /node_modules/, use: [ { - loader: path.join(__dirname, 'loaders', 'externalizeDependencies.js') - } - ] + loader: 'ts-loader', + }, + ], }, { - test: /\.ts$/, - exclude: /node_modules/, + test: /\.node$/, use: [ { - loader: 'ts-loader' - } - ] + loader: 'node-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/**/*' }]) + test: /\.worker\.js$/, + use: { loader: 'worker-loader' }, + }, + ], + }, + externals: [ + 'vscode', + 'commonjs', + ...existingModulesInOutDir, + // These dependencies are ignored because we don't use them, and App Insights has try-catch protecting their loading if they don't exist + // See: https://github.com/microsoft/vscode-extension-telemetry/issues/41#issuecomment-598852991 + 'applicationinsights-native-metrics', + '@opentelemetry/tracing', + '@azure/opentelemetry-instrumentation-azure-sdk', + '@opentelemetry/instrumentation', + '@azure/functions-core', ], + plugins: [...common.getDefaultPlugins('extension')], 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 })] + plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], + conditionNames: ['import', 'require', 'node'], }, output: { filename: '[name].js', path: path.resolve(constants.ExtensionRootDir, 'out', 'client'), libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } + 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 index 3ef870b67fd3..a90e9135a605 100644 --- a/build/webpack/webpack.extension.dependencies.config.js +++ b/build/webpack/webpack.extension.dependencies.config.js @@ -1,12 +1,13 @@ // 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}`; @@ -18,50 +19,26 @@ const config = { 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' } - ] + __dirname: false, }, + module: {}, 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' } - ]) + // eslint-disable-next-line new-cap + new copyWebpackPlugin({ patterns: [{ from: './package.json', to: '.' }] }), ], 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'] + extensions: ['.js'], }, output: { filename: '[name].js', path: path.resolve(constants.ExtensionRootDir, 'out', 'client'), libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } + devtoolModuleFilenameTemplate: '../../[resource-path]', + }, }; -// tslint:disable-next-line:no-default-export + exports.default = config; diff --git a/cgmanifest.json b/cgmanifest.json new file mode 100644 index 000000000000..57123f566794 --- /dev/null +++ b/cgmanifest.json @@ -0,0 +1,15 @@ +{ + "Registrations": [ + { + "Component": { + "Other": { + "Name": "get-pip", + "Version": "21.3.1", + "DownloadUrl": "https://github.com/pypa/get-pip" + }, + "Type": "other" + }, + "DevelopmentDependency": false + } + ] +} diff --git a/customEditor.json b/customEditor.json deleted file mode 100644 index c539fe3a3a7a..000000000000 --- a/customEditor.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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 deleted file mode 100644 index 99acc159fcaa..000000000000 --- a/data/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "/usr/bin/python3" -} diff --git a/data/test.py b/data/test.py deleted file mode 100644 index 3b316dc1e8d1..000000000000 --- a/data/test.py +++ /dev/null @@ -1,2 +0,0 @@ -#%% -print('hello') diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000000..8e1aa990a2c2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,393 @@ +/** + * ESLint Configuration for VS Code Python Extension + * This file configures linting rules for the TypeScript/JavaScript codebase. + * It uses the new flat config format introduced in ESLint 8.21.0 + */ + +// Import essential ESLint plugins and configurations +import tseslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import noOnlyTests from 'eslint-plugin-no-only-tests'; +import prettier from 'eslint-config-prettier'; +import importPlugin from 'eslint-plugin-import'; +import js from '@eslint/js'; +import noBadGdprCommentPlugin from './.eslintplugin/no-bad-gdpr-comment.js'; // Ensure the path is correct + +export default [ + { + ignores: ['**/node_modules/**', '**/out/**'], + }, + // Base configuration for all files + { + ignores: [ + '**/node_modules/**', + '**/out/**', + 'src/test/analysisEngineTest.ts', + 'src/test/ciConstants.ts', + 'src/test/common.ts', + 'src/test/constants.ts', + 'src/test/core.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/performanceTest.ts', + 'src/test/proc.ts', + 'src/test/smokeTest.ts', + 'src/test/standardTest.ts', + 'src/test/startupTelemetry.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/virtualEnvs/condaInheritEnvPrompt.unit.test.ts', + 'src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts', + 'src/test/interpreters/activation/service.unit.test.ts', + 'src/test/interpreters/helpers.unit.test.ts', + 'src/test/interpreters/display.unit.test.ts', + 'src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts', + 'src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts', + 'src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts', + 'src/test/activation/activeResource.unit.test.ts', + 'src/test/activation/extensionSurvey.unit.test.ts', + 'src/test/utils/fs.ts', + 'src/test/api.functional.test.ts', + 'src/test/testing/common/debugLauncher.unit.test.ts', + 'src/test/testing/common/services/configSettingService.unit.test.ts', + 'src/test/common/exitCIAfterTestReporter.ts', + 'src/test/common/terminals/activator/index.unit.test.ts', + 'src/test/common/terminals/activator/base.unit.test.ts', + 'src/test/common/terminals/shellDetector.unit.test.ts', + 'src/test/common/terminals/service.unit.test.ts', + 'src/test/common/terminals/helper.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/terminalActivation.testvirtualenvs.ts', + 'src/test/common/socketStream.test.ts', + 'src/test/common/configSettings.test.ts', + 'src/test/common/experiments/telemetry.unit.test.ts', + 'src/test/common/platform/filesystem.unit.test.ts', + 'src/test/common/platform/errors.unit.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/filesystem.functional.test.ts', + 'src/test/common/platform/filesystem.test.ts', + 'src/test/common/utils/cacheUtils.unit.test.ts', + 'src/test/common/utils/decorators.unit.test.ts', + 'src/test/common/utils/version.unit.test.ts', + 'src/test/common/configSettings/configSettings.unit.test.ts', + 'src/test/common/serviceRegistry.unit.test.ts', + 'src/test/common/extensions.unit.test.ts', + 'src/test/common/variables/envVarsService.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/pipInstaller.unit.test.ts', + 'src/test/common/installer/pipEnvInstaller.unit.test.ts', + 'src/test/common/socketCallbackHandler.test.ts', + 'src/test/common/process/decoder.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/logger.unit.test.ts', + 'src/test/common/process/proc.exec.test.ts', + 'src/test/common/process/pythonProcess.unit.test.ts', + 'src/test/common/process/proc.unit.test.ts', + 'src/test/common/interpreterPathService.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/factory.unit.test.ts', + 'src/test/debugger/extension/adapter/logging.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts', + 'src/test/debugger/utils.ts', + 'src/test/debugger/envVars.test.ts', + 'src/test/telemetry/index.unit.test.ts', + 'src/test/telemetry/envFileTelemetry.unit.test.ts', + 'src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts', + 'src/test/application/diagnostics/checks/envPathVariable.unit.test.ts', + 'src/test/application/diagnostics/applicationDiagnostics.unit.test.ts', + 'src/test/application/diagnostics/promptHandler.unit.test.ts', + 'src/test/application/diagnostics/commands/ignore.unit.test.ts', + 'src/test/performance/load.perf.test.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/base.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts', + 'src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.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/helpers.ts', + 'src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts', + 'src/client/interpreter/display/index.ts', + 'src/client/extension.ts', + 'src/client/startupTelemetry.ts', + 'src/client/terminals/codeExecution/terminalCodeExecution.ts', + 'src/client/terminals/codeExecution/codeExecutionManager.ts', + 'src/client/terminals/codeExecution/djangoContext.ts', + 'src/client/activation/commands.ts', + 'src/client/activation/progress.ts', + 'src/client/activation/extensionSurvey.ts', + 'src/client/activation/common/analysisOptions.ts', + 'src/client/activation/languageClientMiddleware.ts', + 'src/client/testing/serviceRegistry.ts', + 'src/client/testing/main.ts', + 'src/client/testing/configurationFactory.ts', + 'src/client/testing/common/constants.ts', + 'src/client/testing/common/testUtils.ts', + 'src/client/common/helpers.ts', + 'src/client/common/net/browser.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/contextKey.ts', + 'src/client/common/experiments/telemetry.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/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/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/utils/decorators.ts', + 'src/client/common/utils/enum.ts', + 'src/client/common/utils/platform.ts', + 'src/client/common/utils/stopWatch.ts', + 'src/client/common/utils/random.ts', + 'src/client/common/utils/sysTypes.ts', + 'src/client/common/utils/misc.ts', + 'src/client/common/utils/cacheUtils.ts', + 'src/client/common/utils/workerPool.ts', + 'src/client/common/extensions.ts', + 'src/client/common/variables/serviceRegistry.ts', + 'src/client/common/variables/environment.ts', + 'src/client/common/variables/types.ts', + 'src/client/common/variables/systemVariables.ts', + 'src/client/common/cancellation.ts', + 'src/client/common/interpreterPathService.ts', + 'src/client/common/application/applicationShell.ts', + 'src/client/common/application/languageService.ts', + 'src/client/common/application/clipboard.ts', + 'src/client/common/application/workspace.ts', + 'src/client/common/application/debugSessionTelemetry.ts', + 'src/client/common/application/documentManager.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/installer/serviceRegistry.ts', + 'src/client/common/installer/channelManager.ts', + 'src/client/common/installer/moduleInstaller.ts', + 'src/client/common/installer/types.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/pythonToolService.ts', + 'src/client/common/process/internal/python.ts', + 'src/client/common/process/internal/scripts/testing_tools.ts', + 'src/client/common/process/types.ts', + 'src/client/common/process/logger.ts', + 'src/client/common/process/pythonProcess.ts', + 'src/client/common/process/pythonEnvironment.ts', + 'src/client/common/process/decoder.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/activator.ts', + 'src/client/debugger/extension/adapter/logging.ts', + 'src/client/debugger/extension/hooks/eventHandlerDispatcher.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/psProcessParser.ts', + 'src/client/debugger/extension/attachQuickPick/picker.ts', + 'src/client/application/serviceRegistry.ts', + 'src/client/application/diagnostics/base.ts', + 'src/client/application/diagnostics/applicationDiagnostics.ts', + 'src/client/application/diagnostics/filter.ts', + 'src/client/application/diagnostics/promptHandler.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/launchBrowser.ts', + ], + linterOptions: { + reportUnusedDisableDirectives: 'off', + }, + rules: { + ...js.configs.recommended.rules, + 'no-undef': 'off', + }, + }, + // TypeScript-specific configuration + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', 'src', 'pythonExtensionApi/src'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + globals: { + ...(js.configs.recommended.languageOptions?.globals || {}), + mocha: true, + require: 'readonly', + process: 'readonly', + exports: 'readonly', + module: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + 'no-only-tests': noOnlyTests, + import: importPlugin, + prettier: prettier, + 'no-bad-gdpr-comment': noBadGdprCommentPlugin, // Register your plugin + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + }, + }, + rules: { + 'no-bad-gdpr-comment/no-bad-gdpr-comment': 'warn', // Enable your rule + // Base configurations + ...tseslint.configs.recommended.rules, + ...prettier.rules, + + // TypeScript-specific rules + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-ignore': 'allow-with-description', + }, + ], + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-loss-of-precision': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + + // Import rules + 'import/extensions': 'off', + 'import/namespace': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/no-unresolved': 'off', + 'import/prefer-default-export': 'off', + + // Testing rules + 'no-only-tests/no-only-tests': [ + 'error', + { + block: ['test', 'suite'], + focus: ['only'], + }, + ], + + // Code style rules + 'linebreak-style': 'off', + 'no-bitwise': 'off', + 'no-console': 'off', + 'no-underscore-dangle': 'off', + 'operator-assignment': 'off', + 'func-names': 'off', + + // Error handling and control flow + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-async-promise-executor': 'off', + 'no-await-in-loop': 'off', + 'no-unreachable': 'off', + 'no-void': 'off', + + // Duplicates and overrides (TypeScript handles these) + 'no-dupe-class-members': 'off', + 'no-redeclare': 'off', + 'no-undef': 'off', + + // Miscellaneous rules + 'no-control-regex': 'off', + 'no-extend-native': 'off', + 'no-inner-declarations': 'off', + 'no-multi-str': 'off', + 'no-param-reassign': 'off', + 'no-prototype-builtins': 'off', + 'no-empty-function': 'off', + 'no-template-curly-in-string': 'off', + 'no-useless-escape': 'off', + 'no-extra-parentheses': 'off', + 'no-extra-paren': 'off', + '@typescript-eslint/no-extra-parens': 'off', + strict: 'off', + + // Restricted syntax + 'no-restricted-syntax': [ + 'error', + { + selector: 'ForInStatement', + message: + 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', + }, + { + selector: 'LabeledStatement', + message: + 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: + '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', + }, + ], + }, + }, +]; diff --git a/experiments.json b/experiments.json deleted file mode 100644 index 2ce03b4ed5e8..000000000000 --- a/experiments.json +++ /dev/null @@ -1,182 +0,0 @@ -[ - { - "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 index 96a2a6459be8..0b919f16572a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,100 +9,59 @@ '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 { argv } = require('yargs'); const os = require('os'); -const rmrf = require('rimraf'); +const typescript = require('typescript'); -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 tsProject = ts.createProject('./tsconfig.json', { typescript }); -const indentationFilter = ['src/**/*.ts*', '!**/typings/**/*']; - -const tslintFilter = [ - 'src/**/*.ts*', - 'test/**/*.ts*', - '!**/node_modules/**', - '!out/**/*', - '!images/**/*', - '!.vscode/**/*', - '!pythonFiles/**/*', - '!resources/**/*', - '!snippets/**/*', - '!syntaxes/**/*', - '!**/typings/**/*', - '!**/*.d.ts' -]; +const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; -gulp.task('compile', (done) => { +gulp.task('compileCore', (done) => { let failed = false; - const tsProject = ts.createProject('tsconfig.json'); tsProject .src() .pipe(tsProject()) - .on('error', () => (failed = true)) + .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('compileApi', (done) => { + spawnAsync('npm', ['run', 'compileApi'], undefined, true) + .then((stdout) => { + if (stdout.includes('error')) { + done(new Error(stdout)); + } else { + done(); + } + }) + .catch((ex) => { + console.log(ex); + done(new Error('TypeScript compilation errors', ex)); + }); +}); -gulp.task('hygiene-all', (done) => run({ mode: 'all' }, done)); +gulp.task('compile', gulp.series('compileCore', 'compileApi')); -gulp.task('hygiene-branch', (done) => run({ mode: 'diffMaster' }, done)); +gulp.task('precommit', (done) => run({ exitOnError: true, mode: 'staged' }, done)); gulp.task('output:clean', () => del(['coverage'])); -gulp.task('clean:cleanExceptTests', () => del(['clean:vsix', 'out/client', 'out/datascience-ui', 'out/server'])); +gulp.task('clean:cleanExceptTests', () => del(['clean:vsix', 'out/client'])); 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')); @@ -113,54 +72,8 @@ gulp.task('checkNativeDependencies', (done) => { 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); @@ -171,15 +84,40 @@ async function buildWebPackForDevOrProduction(configFile, configNameForProductio 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'); + await buildWebPackForDevOrProduction('./build/webpack/webpack.extension.browser.config.js', 'browser'); +}); + +gulp.task('addExtensionPackDependencies', async () => { + await buildLicense(); + await addExtensionPackDependencies(); }); +async function addExtensionPackDependencies() { + // Update the package.json to add extension pack dependencies at build time so that + // extension dependencies need not be installed during development + const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); + const packageJson = JSON.parse(packageJsonContents); + packageJson.extensionPack = [ + 'ms-python.vscode-pylance', + 'ms-python.debugpy', + 'ms-python.vscode-python-envs', + ].concat(packageJson.extensionPack ? packageJson.extensionPack : []); + // Remove potential duplicates. + packageJson.extensionPack = packageJson.extensionPack.filter( + (item, index) => packageJson.extensionPack.indexOf(item) === index, + ); + await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); +} + +async function buildLicense() { + const headerPath = path.join(__dirname, 'build', 'license-header.txt'); + const licenseHeader = await fsExtra.readFile(headerPath, 'utf-8'); + const license = await fsExtra.readFile('LICENSE', 'utf-8'); + + await fsExtra.writeFile('LICENSE', `${licenseHeader}\n${license}`, 'utf-8'); +} + gulp.task('updateBuildNumber', async () => { await updateBuildNumber(argv); }); @@ -191,14 +129,14 @@ async function updateBuildNumber(args) { const packageJson = JSON.parse(packageJsonContents); // Change version number - const versionParts = packageJson['version'].split('.'); + 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; + : packageJson.version; + packageJson.version = newVersion; // Write back to the package json await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); @@ -208,7 +146,7 @@ async function updateBuildNumber(args) { const changeLogContents = await fsExtra.readFile('CHANGELOG.md', 'utf-8'); const fixedContents = changeLogContents.replace( /##\s*(\d+)\.(\d+)\.(\d+)\s*\(/, - `## $1.$2.${buildNumberPortion} (` + `## $1.$2.${buildNumberPortion} (`, ); // Write back to changelog.md @@ -225,7 +163,7 @@ async function buildWebPack(webpackConfigName, args, env) { const stdOut = await spawnAsync( 'npm', ['run', 'webpack', '--', ...args, ...['--mode', 'production', '--devtool', 'source-map']], - env + env, ); const stdOutLines = stdOut .split(os.EOL) @@ -237,8 +175,8 @@ async function buildWebPack(webpackConfigName, args, env) { .filter( (item) => allowedWarnings.findIndex((allowedWarning) => - item.toLowerCase().startsWith(allowedWarning.toLowerCase()) - ) == -1 + item.toLowerCase().startsWith(allowedWarning.toLowerCase()), + ) === -1, ); const errors = stdOutLines.some((item) => item.startsWith('ERROR in')); if (errors) { @@ -246,7 +184,7 @@ async function buildWebPack(webpackConfigName, args, env) { } if (warnings.length > 0) { throw new Error( - `Warnings in ${webpackConfigName}, Check gulpfile.js to see if the warning should be allowed., \n\n${stdOut}` + `Warnings in ${webpackConfigName}, Check gulpfile.js to see if the warning should be allowed., \n\n${stdOut}`, ); } } @@ -257,163 +195,48 @@ function getAllowedWarningsForWebPack(buildConfig) { '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' + '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' + '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' + 'WARNING in ./node_modules/applicationinsights/out/AutoCollection/NativePerformance.js', + ]; + case 'browser': + 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:', ]; 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) { + 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`) -); +gulp.task('prePublishBundle', gulp.series('webpack')); +gulp.task('checkDependencies', gulp.series('checkNativeDependencies')); +gulp.task('prePublishNonBundle', gulp.series('compile')); function spawnAsync(command, args, env, rejectOnStdErr = false) { env = env || {}; @@ -440,206 +263,6 @@ function spawnAsync(command, args, env, rejectOnStdErr = false) { }); } -/** - * 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) { @@ -648,15 +271,15 @@ function hasNativeDependencies() { 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) + 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) => item !== 'fsevents') .filter( (item) => jsonProperties.findIndex((flattenedDependency) => - flattenedDependency.endsWith(`dependencies.${item}.version`) - ) >= 0 + flattenedDependency.endsWith(`dependencies.${item}.version`), + ) >= 0, ); if (nativeDependencies.length > 0) { console.error('Native dependencies detected', nativeDependencies); @@ -664,443 +287,3 @@ function hasNativeDependencies() { } return false; } - -/** - * @typedef {Object} hygieneOptions - creates a new type named 'SpecialType' - * @property {'changes'|'staged'|'all'|'compile'|'diffMaster'} [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 !== 'master') { - 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 master of current (assumed 'origin') repo. - try { - cp.execSync(`git remote set-branches --add ${originOrUpstream} master`, { - encoding: 'utf8', - cwd: __dirname - }); - cp.execSync('git fetch', { encoding: 'utf8', cwd: __dirname }); - } catch (ex) { - return []; - } - const cmd = `git diff --name-only HEAD ${originOrUpstream}/master`; - 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 getDifferentFromMasterFilesSync() { - const out = git(['diff', '--name-status', 'master']); - 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 === 'diffMaster') { - return getDifferentFromMasterFilesSync().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/images/ConfigureDebugger.gif b/images/ConfigureDebugger.gif index 359e1a7493fd..41113d65896d 100644 Binary files a/images/ConfigureDebugger.gif and b/images/ConfigureDebugger.gif differ diff --git a/images/ConfigureTests.gif b/images/ConfigureTests.gif index 6da0100d593b..38ae2db551e1 100644 Binary files a/images/ConfigureTests.gif and b/images/ConfigureTests.gif differ diff --git a/images/InterpreterSelectionZoom.gif b/images/InterpreterSelectionZoom.gif index 576a438a7d3b..dc5db03aad3d 100644 Binary files a/images/InterpreterSelectionZoom.gif and b/images/InterpreterSelectionZoom.gif differ diff --git a/images/OpenOrCreateNotebook.gif b/images/OpenOrCreateNotebook.gif index 98256a97f190..a0957d415d7d 100644 Binary files a/images/OpenOrCreateNotebook.gif and b/images/OpenOrCreateNotebook.gif differ diff --git a/images/addIcon.PNG b/images/addIcon.PNG new file mode 100644 index 000000000000..8027e617e9ec Binary files /dev/null and b/images/addIcon.PNG differ diff --git a/images/codeIcon.PNG b/images/codeIcon.PNG new file mode 100644 index 000000000000..7ad46cee077f Binary files /dev/null and b/images/codeIcon.PNG differ diff --git a/images/dataViewerIcon.PNG b/images/dataViewerIcon.PNG new file mode 100644 index 000000000000..6848c600794d Binary files /dev/null and b/images/dataViewerIcon.PNG differ diff --git a/images/dataviewer.gif b/images/dataviewer.gif index b2f5e080a401..ce0c81676c09 100644 Binary files a/images/dataviewer.gif and b/images/dataviewer.gif differ diff --git a/images/exportIcon.PNG b/images/exportIcon.PNG new file mode 100644 index 000000000000..e5e588040ee6 Binary files /dev/null and b/images/exportIcon.PNG differ diff --git a/images/kernelchange.gif b/images/kernelchange.gif index a414e2252efa..d2b753b84c09 100644 Binary files a/images/kernelchange.gif and b/images/kernelchange.gif differ diff --git a/images/markdownIcon.PNG b/images/markdownIcon.PNG new file mode 100644 index 000000000000..04e5d67749db Binary files /dev/null and b/images/markdownIcon.PNG differ diff --git a/images/playIcon.PNG b/images/playIcon.PNG new file mode 100644 index 000000000000..60ae4a2051df Binary files /dev/null and b/images/playIcon.PNG differ diff --git a/images/plotViewerIcon.PNG b/images/plotViewerIcon.PNG new file mode 100644 index 000000000000..e8ecf0d97b5e Binary files /dev/null and b/images/plotViewerIcon.PNG differ diff --git a/images/plotviewer.gif b/images/plotviewer.gif index d1f6524f3769..a3c438b761e0 100644 Binary files a/images/plotviewer.gif and b/images/plotviewer.gif differ diff --git a/images/remoteserver.gif b/images/remoteserver.gif index be836e673b72..f979d557aa6b 100644 Binary files a/images/remoteserver.gif and b/images/remoteserver.gif differ diff --git a/images/savetopythonfile.png b/images/savetopythonfile.png index ccb0a7bfa387..e4a7f08d3db0 100644 Binary files a/images/savetopythonfile.png and b/images/savetopythonfile.png differ diff --git a/images/variableExplorerIcon.PNG b/images/variableExplorerIcon.PNG new file mode 100644 index 000000000000..f8363dda9de4 Binary files /dev/null and b/images/variableExplorerIcon.PNG differ diff --git a/images/variableexplorer.png b/images/variableexplorer.png index 0f6c9f73b4de..31197571b796 100644 Binary files a/images/variableexplorer.png and b/images/variableexplorer.png differ diff --git a/news/.vscode/settings.json b/news/.vscode/settings.json deleted file mode 100644 index 2b759d72bd35..000000000000 --- a/news/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "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/10496.md b/news/1 Enhancements/10496.md deleted file mode 100644 index e486012325cb..000000000000 --- a/news/1 Enhancements/10496.md +++ /dev/null @@ -1,4 +0,0 @@ -Opening notebooks in the preview Notebook editor for [Visual Studio Code Insiders](https://code.visualstudio.com/insiders/). -* Install Python extension in the latest [Visual Studio Code Insiders](https://code.visualstudio.com/insiders/). -* Wait for `Python Extension` to get activated (e.g. open a `Python` file). -* Right click on an `*.ipynb (Jupyter Notebook)` file and select `Open in preview Notebook Editor`. diff --git a/news/1 Enhancements/12414.md b/news/1 Enhancements/12414.md deleted file mode 100644 index da6a9071c367..000000000000 --- a/news/1 Enhancements/12414.md +++ /dev/null @@ -1 +0,0 @@ -Add cell editing shortcuts for python interactive cells. Thanks [@earthastronaut](https://github.com/earthastronaut/)! diff --git a/news/1 Enhancements/12827.md b/news/1 Enhancements/12827.md deleted file mode 100644 index 88caf294fd52..000000000000 --- a/news/1 Enhancements/12827.md +++ /dev/null @@ -1 +0,0 @@ -Allow `python.dataScience.runStartupCommands` to be an array. (thanks @janosh) diff --git a/news/1 Enhancements/12828.md b/news/1 Enhancements/12828.md deleted file mode 100644 index 8699646902db..000000000000 --- a/news/1 Enhancements/12828.md +++ /dev/null @@ -1 +0,0 @@ -Remember remote kernel ids when reopening notebooks. \ No newline at end of file diff --git a/news/1 Enhancements/12959.md b/news/1 Enhancements/12959.md deleted file mode 100644 index 4cb6df946b7e..000000000000 --- a/news/1 Enhancements/12959.md +++ /dev/null @@ -1 +0,0 @@ -The file explorer dialog now has an appropriate title when browsing for an interpreter. (thanks [ziebam](https://github.com/ziebam)) diff --git a/news/1 Enhancements/12980.md b/news/1 Enhancements/12980.md deleted file mode 100644 index cdf47f662783..000000000000 --- a/news/1 Enhancements/12980.md +++ /dev/null @@ -1 +0,0 @@ -Warn users if they are connecting over http without a token. \ No newline at end of file diff --git a/news/1 Enhancements/12988.md b/news/1 Enhancements/12988.md deleted file mode 100644 index 025fbf5f0997..000000000000 --- a/news/1 Enhancements/12988.md +++ /dev/null @@ -1 +0,0 @@ -Allow a custom display string for remote servers as part of the remote Jupyter server provider extensibility point. diff --git a/news/1 Enhancements/13037.md b/news/1 Enhancements/13037.md deleted file mode 100644 index 7361a0fa2803..000000000000 --- a/news/1 Enhancements/13037.md +++ /dev/null @@ -1 +0,0 @@ -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/)) diff --git a/news/1 Enhancements/13122.md b/news/1 Enhancements/13122.md deleted file mode 100644 index 2d36013ce0d7..000000000000 --- a/news/1 Enhancements/13122.md +++ /dev/null @@ -1 +0,0 @@ -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. diff --git a/news/1 Enhancements/13218.md b/news/1 Enhancements/13218.md deleted file mode 100644 index 7019f17603c2..000000000000 --- a/news/1 Enhancements/13218.md +++ /dev/null @@ -1 +0,0 @@ -Added "pythonArgs" to debugpy launch.json schema. diff --git a/news/1 Enhancements/13259.md b/news/1 Enhancements/13259.md deleted file mode 100644 index 191e413fee01..000000000000 --- a/news/1 Enhancements/13259.md +++ /dev/null @@ -1 +0,0 @@ -Use jupyter inspect to get signature of dynamic functions in notebook editor when language server doesn't provide enough hint. diff --git a/news/1 Enhancements/3073.md b/news/1 Enhancements/3073.md deleted file mode 100644 index afd0809180a4..000000000000 --- a/news/1 Enhancements/3073.md +++ /dev/null @@ -1 +0,0 @@ -Add "Restart Language Server" command diff --git a/news/1 Enhancements/3104.md b/news/1 Enhancements/3104.md deleted file mode 100644 index 84879f7a8caa..000000000000 --- a/news/1 Enhancements/3104.md +++ /dev/null @@ -1 +0,0 @@ -Support multiple and per file interactive windows. See the description for the new 'python.dataScience.interactiveWindowMode' setting. \ No newline at end of file diff --git a/news/1 Enhancements/README.md b/news/1 Enhancements/README.md deleted file mode 100644 index 2a159b65f4f0..000000000000 --- a/news/1 Enhancements/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Changes that add new features. - diff --git a/news/2 Fixes/10924.md b/news/2 Fixes/10924.md deleted file mode 100644 index c900a8a4a7f8..000000000000 --- a/news/2 Fixes/10924.md +++ /dev/null @@ -1 +0,0 @@ -Gathered notebooks will now use the same kernelspec as the notebook it was created from. diff --git a/news/2 Fixes/11122.md b/news/2 Fixes/11122.md deleted file mode 100644 index 7304c9aac45a..000000000000 --- a/news/2 Fixes/11122.md +++ /dev/null @@ -1 +0,0 @@ -Check for hideFromUser before activating current terminal. diff --git a/news/2 Fixes/11743.md b/news/2 Fixes/11743.md deleted file mode 100644 index fc58b29547e7..000000000000 --- a/news/2 Fixes/11743.md +++ /dev/null @@ -1 +0,0 @@ -Don't loop selection through all failed tests every time tests are run. diff --git a/news/2 Fixes/11875.md b/news/2 Fixes/11875.md deleted file mode 100644 index 09fee50c8f4f..000000000000 --- a/news/2 Fixes/11875.md +++ /dev/null @@ -1,4 +0,0 @@ -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). diff --git a/news/2 Fixes/12202.md b/news/2 Fixes/12202.md deleted file mode 100644 index 8d7778c72e80..000000000000 --- a/news/2 Fixes/12202.md +++ /dev/null @@ -1 +0,0 @@ -Fix missing css for some ipywidget output. \ No newline at end of file diff --git a/news/2 Fixes/12510.md b/news/2 Fixes/12510.md deleted file mode 100644 index db76bcceacd9..000000000000 --- a/news/2 Fixes/12510.md +++ /dev/null @@ -1,2 +0,0 @@ -Delete backing untitled ipynb notebook files as soon as the remote session has been created. - diff --git a/news/2 Fixes/12766.md b/news/2 Fixes/12766.md deleted file mode 100644 index 2f065cd04279..000000000000 --- a/news/2 Fixes/12766.md +++ /dev/null @@ -1 +0,0 @@ -Make the data science variable explorer support high contrast color theme. \ No newline at end of file diff --git a/news/2 Fixes/12821.md b/news/2 Fixes/12821.md deleted file mode 100644 index 5951593948e9..000000000000 --- a/news/2 Fixes/12821.md +++ /dev/null @@ -1 +0,0 @@ -Use the given interpreter for launching the non-daemon python \ No newline at end of file diff --git a/news/2 Fixes/12833.md b/news/2 Fixes/12833.md deleted file mode 100644 index 7cfb9576133e..000000000000 --- a/news/2 Fixes/12833.md +++ /dev/null @@ -1,3 +0,0 @@ -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. diff --git a/news/2 Fixes/12931.md b/news/2 Fixes/12931.md deleted file mode 100644 index ad73355a8bc6..000000000000 --- a/news/2 Fixes/12931.md +++ /dev/null @@ -1 +0,0 @@ -Refactor data science filesystem usage to correctly handle files which are potentially remote. diff --git a/news/2 Fixes/12987.md b/news/2 Fixes/12987.md deleted file mode 100644 index e048532fc1fc..000000000000 --- a/news/2 Fixes/12987.md +++ /dev/null @@ -1 +0,0 @@ -Allow custom Jupyter server URI providers to have an expiration on their authorization headers. \ No newline at end of file diff --git a/news/2 Fixes/13106.md b/news/2 Fixes/13106.md deleted file mode 100644 index 68189e04e0a5..000000000000 --- a/news/2 Fixes/13106.md +++ /dev/null @@ -1 +0,0 @@ -If a webpanel fails to load, dispose our webviewhost so that it can try again. \ No newline at end of file diff --git a/news/2 Fixes/13117.md b/news/2 Fixes/13117.md deleted file mode 100644 index 2ed818ea73c2..000000000000 --- a/news/2 Fixes/13117.md +++ /dev/null @@ -1 +0,0 @@ -Ensure terminal is not shown or activated if hideFromUser is set to true. diff --git a/news/2 Fixes/13124.md b/news/2 Fixes/13124.md deleted file mode 100644 index ddf8f44a41e7..000000000000 --- a/news/2 Fixes/13124.md +++ /dev/null @@ -1 +0,0 @@ -Do not automatically start kernel for untrusted notebooks. diff --git a/news/2 Fixes/13156.md b/news/2 Fixes/13156.md deleted file mode 100644 index e0282e345b32..000000000000 --- a/news/2 Fixes/13156.md +++ /dev/null @@ -1 +0,0 @@ -Fix settings links to open correctly in the notebook editor. \ No newline at end of file diff --git a/news/2 Fixes/13165.md b/news/2 Fixes/13165.md deleted file mode 100644 index 862f069ba486..000000000000 --- a/news/2 Fixes/13165.md +++ /dev/null @@ -1 +0,0 @@ -"a" and "b" Jupyter shortcuts should not automatically enter edit mode. diff --git a/news/2 Fixes/13172.md b/news/2 Fixes/13172.md deleted file mode 100644 index a37e35a29784..000000000000 --- a/news/2 Fixes/13172.md +++ /dev/null @@ -1 +0,0 @@ -Scope custom notebook keybindings to Jupyter Notebooks. diff --git a/news/2 Fixes/13205.md b/news/2 Fixes/13205.md deleted file mode 100644 index f6828574a80f..000000000000 --- a/news/2 Fixes/13205.md +++ /dev/null @@ -1 +0,0 @@ -Rename "Count" column in variable explorer to "Size". diff --git a/news/2 Fixes/13235.md b/news/2 Fixes/13235.md deleted file mode 100644 index 8c6588adfb1d..000000000000 --- a/news/2 Fixes/13235.md +++ /dev/null @@ -1 +0,0 @@ -Handle `Save As` of preview Notebooks. diff --git a/news/2 Fixes/README.md b/news/2 Fixes/README.md deleted file mode 100644 index cc5e1020961d..000000000000 --- a/news/2 Fixes/README.md +++ /dev/null @@ -1 +0,0 @@ -Changes that fix broken behaviour. diff --git a/news/3 Code Health/10772.md b/news/3 Code Health/10772.md deleted file mode 100644 index e69cb3962764..000000000000 --- a/news/3 Code Health/10772.md +++ /dev/null @@ -1 +0,0 @@ -Move non-mock jupyter nightly tests to use raw kernel by default. \ No newline at end of file diff --git a/news/3 Code Health/12554.md b/news/3 Code Health/12554.md deleted file mode 100644 index 926f9f320057..000000000000 --- a/news/3 Code Health/12554.md +++ /dev/null @@ -1 +0,0 @@ -Add tests for trusted notebooks. diff --git a/news/3 Code Health/12809.md b/news/3 Code Health/12809.md deleted file mode 100644 index 9c28e7d10f0c..000000000000 --- a/news/3 Code Health/12809.md +++ /dev/null @@ -1 +0,0 @@ -Add new services to data science IOC container and rename misspelled service. \ No newline at end of file diff --git a/news/3 Code Health/12844.md b/news/3 Code Health/12844.md deleted file mode 100644 index 004246586d91..000000000000 --- a/news/3 Code Health/12844.md +++ /dev/null @@ -1 +0,0 @@ -Update categories in `package.json`. diff --git a/news/3 Code Health/12893.md b/news/3 Code Health/12893.md deleted file mode 100644 index 5a4d62330a56..000000000000 --- a/news/3 Code Health/12893.md +++ /dev/null @@ -1 +0,0 @@ -Disable Notebook icons when Notebook is not trusted. diff --git a/news/3 Code Health/12919.md b/news/3 Code Health/12919.md deleted file mode 100644 index ed99e77bef9b..000000000000 --- a/news/3 Code Health/12919.md +++ /dev/null @@ -1 +0,0 @@ -Removed control tower code for the start page. diff --git a/news/3 Code Health/12966.md b/news/3 Code Health/12966.md deleted file mode 100644 index d7c5d9b68a90..000000000000 --- a/news/3 Code Health/12966.md +++ /dev/null @@ -1 +0,0 @@ -Add better tests for trusted notebooks in the classic notebook editor. diff --git a/news/3 Code Health/12977.md b/news/3 Code Health/12977.md deleted file mode 100644 index 5988a016f732..000000000000 --- a/news/3 Code Health/12977.md +++ /dev/null @@ -1 +0,0 @@ -Custom renderers for `png/jpeg` images in `Notebooks`. diff --git a/news/3 Code Health/13075.md b/news/3 Code Health/13075.md deleted file mode 100644 index 2d7e1887b5b0..000000000000 --- a/news/3 Code Health/13075.md +++ /dev/null @@ -1 +0,0 @@ -Fix broken nightly variable explorer tests. \ No newline at end of file diff --git a/news/3 Code Health/13171.md b/news/3 Code Health/13171.md deleted file mode 100644 index 8fcf0a30fb9e..000000000000 --- a/news/3 Code Health/13171.md +++ /dev/null @@ -1 +0,0 @@ -Fix nightly flake test failures for startup and shutdown native editor test. \ No newline at end of file diff --git a/news/3 Code Health/13269.md b/news/3 Code Health/13269.md deleted file mode 100644 index 28e5dff53997..000000000000 --- a/news/3 Code Health/13269.md +++ /dev/null @@ -1 +0,0 @@ -Fix failing interactive window and variable explorer tests. \ No newline at end of file diff --git a/news/3 Code Health/README.md b/news/3 Code Health/README.md deleted file mode 100644 index 10619f41f3a4..000000000000 --- a/news/3 Code Health/README.md +++ /dev/null @@ -1 +0,0 @@ -Changes that should not be user-facing. diff --git a/news/README.md b/news/README.md deleted file mode 100644 index f26d25030fab..000000000000 --- a/news/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# 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 deleted file mode 100644 index b496ec1d0c8c..000000000000 --- a/news/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -# 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 deleted file mode 100644 index d4d7dd1cfd66..000000000000 --- a/news/announce.py +++ /dev/null @@ -1,193 +0,0 @@ -# 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 deleted file mode 100644 index ab0e1cc5187e..000000000000 --- a/news/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -docopt -pytest diff --git a/news/requirements.txt b/news/requirements.txt deleted file mode 100644 index 676ca35073cf..000000000000 --- a/news/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile -# -atomicwrites==1.3.0 # via pytest -attrs==19.3.0 # via pytest -colorama==0.4.1 # via pytest -docopt==0.6.2 -importlib-metadata==1.1.0 # via pluggy, pytest -more-itertools==8.0.0 # via pytest, zipp -packaging==19.2 # via pytest -pluggy==0.13.1 # via pytest -py==1.8.0 # via pytest -pyparsing==2.4.5 # via packaging -pytest==5.3.1 -six==1.13.0 # via packaging -wcwidth==0.1.7 # via pytest -zipp==0.6.0 # via importlib-metadata diff --git a/news/test_announce.py b/news/test_announce.py deleted file mode 100644 index acc125a7c360..000000000000 --- a/news/test_announce.py +++ /dev/null @@ -1,208 +0,0 @@ -# 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/noxfile.py b/noxfile.py new file mode 100644 index 000000000000..3991ee8c025a --- /dev/null +++ b/noxfile.py @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import nox +import shutil +import sys +import sysconfig +import uuid + +EXT_ROOT = pathlib.Path(__file__).parent + + +def delete_dir(path: pathlib.Path, ignore_errors=None): + attempt = 0 + known = [] + while attempt < 5: + try: + shutil.rmtree(os.fspath(path), ignore_errors=ignore_errors) + return + except PermissionError as pe: + if os.fspath(pe.filename) in known: + break + print(f"Changing permissions on {pe.filename}") + os.chmod(pe.filename, 0o666) + + shutil.rmtree(os.fspath(path)) + + +@nox.session() +def install_python_libs(session: nox.Session): + requirements = [ + ("./python_files/lib/python", "./requirements.txt"), + ( + "./python_files/lib/jedilsp", + "./python_files/jedilsp_requirements/requirements.txt", + ), + ] + for target, file in requirements: + session.install( + "-t", + target, + "--no-cache-dir", + "--implementation", + "py", + "--no-deps", + "--require-hashes", + "--only-binary", + ":all:", + "-r", + file, + ) + + session.install("packaging") + session.install("debugpy") + + # Download get-pip script + session.run( + "python", + "./python_files/download_get_pip.py", + env={"PYTHONPATH": "./python_files/lib/temp"}, + ) + + if pathlib.Path("./python_files/lib/temp").exists(): + shutil.rmtree("./python_files/lib/temp") + + +@nox.session() +def native_build(session: nox.Session): + source_dir = pathlib.Path(pathlib.Path.cwd() / "python-env-tools").resolve() + dest_dir = pathlib.Path(pathlib.Path.cwd() / "python-env-tools").resolve() + + with session.cd(source_dir): + if not pathlib.Path(dest_dir / "bin").exists(): + pathlib.Path(dest_dir / "bin").mkdir() + + if not pathlib.Path(dest_dir / "bin" / ".gitignore").exists(): + pathlib.Path(dest_dir / "bin" / ".gitignore").write_text( + "*\n", encoding="utf-8" + ) + + ext = sysconfig.get_config_var("EXE") or "" + target = os.environ.get("CARGO_TARGET", None) + + session.run("cargo", "fetch", external=True) + if target: + session.run( + "cargo", + "build", + "--frozen", + "--release", + "--target", + target, + external=True, + ) + source = source_dir / "target" / target / "release" / f"pet{ext}" + else: + session.run( + "cargo", + "build", + "--frozen", + "--release", + external=True, + ) + source = source_dir / "target" / "release" / f"pet{ext}" + dest = dest_dir / "bin" / f"pet{ext}" + shutil.copy(source, dest) + + # Remove python-env-tools/bin exclusion from .vscodeignore + vscode_ignore = EXT_ROOT / ".vscodeignore" + remove_patterns = ("python-env-tools/bin/**",) + lines = vscode_ignore.read_text(encoding="utf-8").splitlines() + filtered_lines = [line for line in lines if not line.startswith(remove_patterns)] + vscode_ignore.write_text("\n".join(filtered_lines) + "\n", encoding="utf-8") + + +@nox.session() +def checkout_native(session: nox.Session): + dest = (pathlib.Path.cwd() / "python-env-tools").resolve() + if dest.exists(): + shutil.rmtree(os.fspath(dest)) + + temp_dir = os.getenv("TEMP") or os.getenv("TMP") or "/tmp" + temp_dir = pathlib.Path(temp_dir) / str(uuid.uuid4()) / "python-env-tools" + temp_dir.mkdir(0o766, parents=True) + + session.log(f"Cloning python-environment-tools to {temp_dir}") + try: + with session.cd(temp_dir): + session.run("git", "init", external=True) + session.run( + "git", + "remote", + "add", + "origin", + "https://github.com/microsoft/python-environment-tools", + external=True, + ) + session.run("git", "fetch", "origin", "main", external=True) + session.run( + "git", "checkout", "--force", "-B", "main", "origin/main", external=True + ) + delete_dir(temp_dir / ".git") + delete_dir(temp_dir / ".github") + delete_dir(temp_dir / ".vscode") + (temp_dir / "CODE_OF_CONDUCT.md").unlink() + shutil.move(os.fspath(temp_dir), os.fspath(dest)) + except PermissionError as e: + print(f"Permission error: {e}") + if not dest.exists(): + raise + finally: + delete_dir(temp_dir.parent, ignore_errors=True) + + +@nox.session() +def setup_repo(session: nox.Session): + install_python_libs(session) + checkout_native(session) + native_build(session) diff --git a/package-lock.json b/package-lock.json index 4ec8c00c4d7f..6de6edae81c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23068 +1,23251 @@ { "name": "python", - "version": "2020.8.0-dev", - "lockfileVersion": 1, + "version": "2026.5.0-dev", + "lockfileVersion": 2, "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 - } + "packages": { + "": { + "name": "python", + "version": "2026.5.0-dev", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@vscode/extension-telemetry": "^0.8.4", + "arch": "^2.1.0", + "fs-extra": "^11.2.0", + "glob": "^7.2.0", + "iconv-lite": "^0.6.3", + "inversify": "^6.0.2", + "jsonc-parser": "^3.0.0", + "lodash": "^4.18.1", + "minimatch": "^5.1.8", + "named-js-regexp": "^1.3.3", + "node-stream-zip": "^1.6.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^6.5.4", + "rxjs-compat": "^6.5.4", + "semver": "^7.5.2", + "stack-trace": "0.0.10", + "sudo-prompt": "^9.2.1", + "tmp": "^0.2.5", + "uint64be": "^3.0.0", + "unicode": "^14.0.0", + "vscode-debugprotocol": "^1.28.0", + "vscode-jsonrpc": "^9.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.12", + "vscode-languageserver-protocol": "^3.17.6-next.10", + "vscode-tas-client": "^0.1.84", + "which": "^2.0.2", + "winreg": "^1.2.4", + "xml2js": "^0.5.0" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/bent": "^7.3.0", + "@types/chai": "^4.1.2", + "@types/chai-arrays": "^2.0.0", + "@types/chai-as-promised": "^7.1.0", + "@types/download": "^8.0.1", + "@types/fs-extra": "^11.0.4", + "@types/glob": "^7.2.0", + "@types/lodash": "^4.14.104", + "@types/mocha": "^9.1.0", + "@types/node": "^22.19.1", + "@types/semver": "^5.5.0", + "@types/shortid": "^0.0.29", + "@types/sinon": "^17.0.3", + "@types/stack-trace": "0.0.29", + "@types/tmp": "^0.0.33", + "@types/vscode": "^1.95.0", + "@types/which": "^2.0.1", + "@types/winreg": "^1.2.30", + "@types/xml2js": "^0.4.2", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vscode/test-electron": "^2.3.8", + "@vscode/vsce": "^2.27.0", + "bent": "^7.3.12", + "chai": "^4.1.2", + "chai-arrays": "^2.0.0", + "chai-as-promised": "^7.1.1", + "copy-webpack-plugin": "^9.1.0", + "cross-env": "^7.0.3", + "cross-spawn": "^6.0.5", + "del": "^6.0.0", + "download": "^8.0.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-no-only-tests": "^3.3.0", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.0", + "expose-loader": "^3.1.0", + "flat": "^5.0.2", + "get-port": "^5.1.1", + "gulp": "^5.0.0", + "gulp-typescript": "^5.0.0", + "mocha": "^11.1.0", + "mocha-junit-reporter": "^2.0.2", + "mocha-multi-reporters": "^1.1.7", + "node-has-native-dependencies": "^1.0.2", + "node-loader": "^1.0.2", + "node-polyfill-webpack-plugin": "^1.1.4", + "nyc": "^15.0.0", + "prettier": "^2.0.2", + "rewiremock": "^3.13.0", + "shortid": "^2.2.8", + "sinon": "^18.0.0", + "source-map-support": "^0.5.12", + "ts-loader": "^9.2.8", + "ts-mockito": "^2.5.0", + "ts-node": "^10.7.0", + "tsconfig-paths-webpack-plugin": "^3.2.0", + "typemoq": "^2.1.0", + "typescript": "~5.2", + "uuid": "^8.3.2", + "webpack": "^5.105.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^4.9.2", + "webpack-fix-default-import-plugin": "^1.0.3", + "webpack-merge": "^5.8.0", + "webpack-node-externals": "^3.0.0", + "webpack-require-from": "^1.8.6", + "worker-loader": "^3.0.8", + "yargs": "^15.3.1" + }, + "engines": { + "vscode": "^1.95.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.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" - }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "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 - } + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" } }, - "@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" - }, + "node_modules/@azure/abort-controller/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/core-auth": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", "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 - } + "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "@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" - } + "node_modules/@azure/core-auth/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "@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==", + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.1.0", - "@babel/types": "^7.0.0" + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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==", + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, - "requires": { - "@babel/types": "^7.3.0", - "esutils": "^2.0.0" + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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==", + "node_modules/@azure/core-client/node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "@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" + "node_modules/@azure/core-client/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "@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" + "node_modules/@azure/core-rest-pipeline/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" } }, - "@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" + "node_modules/@azure/core-rest-pipeline/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@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" + "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "@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" + "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" } }, - "@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" + "node_modules/@azure/core-tracing/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/core-util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.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" + "node_modules/@azure/core-util/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.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==", + "node_modules/@azure/identity/node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", "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" + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "@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==", + "node_modules/@azure/identity/node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, - "requires": { - "@babel/types": "^7.0.0" + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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==", + "node_modules/@azure/identity/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "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" + "node_modules/@azure/logger": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.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" - } + "node_modules/@azure/logger/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "@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==", + "node_modules/@azure/msal-browser": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.14.0.tgz", + "integrity": "sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==", "dev": true, - "requires": { - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" + "dependencies": { + "@azure/msal-common": "14.10.0" + }, + "engines": { + "node": ">=0.8.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==", + "node_modules/@azure/msal-common": { + "version": "14.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", + "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", "dev": true, - "requires": { - "@babel/types": "^7.4.4" + "engines": { + "node": ">=0.8.0" } }, - "@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==", + "node_modules/@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", "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" + "dependencies": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" } }, - "@babel/helpers": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz", - "integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==", + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", "dev": true, - "requires": { - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.0", - "@babel/types": "^7.5.0" + "engines": { + "node": ">=0.8.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" + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "dependencies": { + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.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 + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "@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==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "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" + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0" + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-json-strings": "^7.2.0" + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "@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==", + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@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==", + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@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==", + "node_modules/@babel/helper-compilation-targets/node_modules/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": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "yallist": "^3.0.2" } }, - "@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" - } + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, - "@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==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "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" + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "lodash": "^4.17.11" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@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==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "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" + "license": "MIT", + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "@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==", + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/runtime-corejs3": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@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==", + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.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==", + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", "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" + "engines": { + "node": ">= 12" } }, - "@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==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "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" + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" } }, - "@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==", + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "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" + "engines": { + "node": ">=10.0.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==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.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==", + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, - "requires": { - "regexp-tree": "^0.1.6" + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "@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==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "@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==", + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0" - } + "license": "Python-2.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==", + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "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" + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@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==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@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==", + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "@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==", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "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" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "@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==", + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } + "license": "MIT" }, - "@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==", + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@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==", + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, - "requires": { - "regenerator-transform": "^0.14.0" + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.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==", + "node_modules/@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "engines": { + "node": ">=10.13.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==", + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", "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" + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "@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==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.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==", + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@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==", + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "@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==", + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } + "license": "MIT" }, - "@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==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "@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==", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" - } + "license": "BSD-3-Clause" + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" }, - "@babel/polyfill": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.4.4.tgz", - "integrity": "sha512-WlthFLfhQQhh+A2Gn5NSFl0Huxz36x86Jn+E9OW7ibK8edKPq+KLy4apM1yDpQ8kJOVi1OVjpP4vSDLdrI04dg==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.2" - }, + "license": "ISC", "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 - } + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "@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==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "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" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "@babel/register": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.9.0.tgz", - "integrity": "sha512-Tv8Zyi2J2VRR8g7pC5gTeIN8Ihultbmk0ocyNz8H2nEZbmhp1N6q0A1UGsQbDvGP/sNinQKUHf3SqXwqjtFv4Q==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "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" + "license": "MIT", + "engines": { + "node": ">=12" }, - "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" - } - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@babel/runtime": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.4.tgz", - "integrity": "sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "requires": { - "regenerator-runtime": "^0.13.2" - } + "license": "MIT" }, - "@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==", + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.2" - }, + "license": "MIT", "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 - } + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@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==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" - }, + "license": "MIT", "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 - } + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "@babel/traverse": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.0.tgz", - "integrity": "sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==", + "node_modules/@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": { - "@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": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, "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" - } - } + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15" } }, - "@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==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", - "to-fast-properties": "^2.0.0" + "engines": { + "node": ">=8" } }, - "@blueprintjs/core": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.22.3.tgz", - "integrity": "sha512-IaxkvJyF+4VCvAjMvyHtJ4qUiQNwgPu4zIxLRo1cBsu30gHjYzwe+xDdssBR7yRnXARguM6bkI523w+yJOerCA==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "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 - } + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "@blueprintjs/icons": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.13.0.tgz", - "integrity": "sha512-fvXGsAJ66SSjeHv3OeXjLEdKdPJ3wVztjhJQCAd51uebhj3FJ16EDDvO7BqBw5FyVkLkU11KAxSoCFZt7TC9GA==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "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 - } + "engines": { + "node": ">=6.0.0" } }, - "@blueprintjs/select": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/@blueprintjs/select/-/select-3.11.2.tgz", - "integrity": "sha512-fU0Km6QI/ayWhzYeu9N1gTj0+L0XUO4KB3u2LfJXgj648UGY8F4HX2ETdJ+XPdtsu6TesrIL7ghMQhtLcvafBg==", + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "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 - } + "engines": { + "node": ">=6.0.0" } }, - "@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==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "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 - } + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "@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==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "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==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, - "requires": { - "@emotion/hash": "^0.6.6", - "@emotion/memoize": "^0.6.6", - "@emotion/unitless": "^0.6.7", - "@emotion/utils": "^0.8.2" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@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 + "node_modules/@microsoft/1ds-core-js": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", + "dependencies": { + "@microsoft/applicationinsights-core-js": "2.8.15", + "@microsoft/applicationinsights-shims": "^2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + } }, - "@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 + "node_modules/@microsoft/1ds-post-js": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", + "dependencies": { + "@microsoft/1ds-core-js": "3.2.13", + "@microsoft/applicationinsights-shims": "^2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + } }, - "@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 + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", + "dependencies": { + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } }, - "@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" + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", "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" - } - } + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "@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" - }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "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" - } - } + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "@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" + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "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 - } + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "peerDependencies": { + "tslib": "*" } }, - "@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 + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", - "dev": true + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } }, - "@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" + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", + "dependencies": { + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.9" + }, + "peerDependencies": { + "tslib": "*" } }, - "@jupyter-widgets/base": { + "node_modules/@microsoft/applicationinsights-shims": { "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" + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz", + "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" + }, + "node_modules/@microsoft/applicationinsights-web-basic": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", + "dependencies": { + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "peerDependencies": { + "tslib": "*" } }, - "@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" + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "peerDependencies": { + "tslib": "*" } }, - "@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 - } + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "@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" + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "@jupyter-widgets/schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@jupyter-widgets/schema/-/schema-0.4.0.tgz", - "integrity": "sha512-0MAZ6hLOCe2dYiUvEAfYvWKD7zV9AdkC4AoIEQiWqAai9Pq06oPNWMMg6x+J0ZaNnZWqR2c16f62ehd57Ql7Zw==" + "node_modules/@microsoft/applicationinsights-web-snippet": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", + "integrity": "sha512-2IHAOaLauc8qaAitvWS+U931T+ze+7MNWrDHY47IENP5y2UA0vqJDu67kWZDdpCN1fFC77sfgfB+HV7SrKshnQ==" }, - "@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" - } - } + "node_modules/@microsoft/dynamicproto-js": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", + "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" + }, + "node_modules/@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" } }, - "@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" - } - } + "node_modules/@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, + "node_modules/@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" } }, - "@jupyterlab/attachments": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@jupyterlab/attachments/-/attachments-1.2.2.tgz", - "integrity": "sha512-ME7aWyEs4piW6tCQbJ4zfuX6XipX07Wb9vVcW7SuRPlDp7lIUi4nPINkMO2us/lTsuS0/ZjgXjb+VKu4CbTYFQ==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "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" - } - } + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "@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" - } - } + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" } }, - "@jupyterlab/codeeditor": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-1.2.0.tgz", - "integrity": "sha512-toejhF/a80X10SZyvEnsnnlS9SxR5W4cz67ju7e/2lsZ8RMwZEDDJAJXyW3mw/EEjt8oVRNP2QpM8L5clE9XyQ==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "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": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", "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" - } - } + "@opentelemetry/semantic-conventions": "1.15.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.5.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" - } - } + "node_modules/@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "dependencies": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.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" - } - } + "node_modules/@opentelemetry/resources": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", + "dependencies": { + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.5.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" - } - } + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", + "dependencies": { + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.5.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" - } - } + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==", + "engines": { + "node": ">=14" } }, - "@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==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "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" - } - } + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" } }, - "@jupyterlab/mainmenu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@jupyterlab/mainmenu/-/mainmenu-1.2.2.tgz", - "integrity": "sha512-hNFvNvsjbqzM1GQp3tF/34j7Y9eR1zoGUuZBsAkge3W0Nsri8fUXfPwBTfOFBXhIx4j9aB4vZhCrgU91eNt7MA==", + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "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" - } - } + "license": "MIT" + }, + "node_modules/@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, + "license": "MIT", + "engines": { + "node": ">=4" } }, - "@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" - } - } + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" } }, - "@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" - }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, "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" - } - } + "@sinonjs/commons": "^3.0.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" - } - } + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" } }, - "@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" - } - } + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" } }, - "@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==", + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@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, - "requires": { - "@phosphor/coreutils": "^1.3.1", - "@phosphor/widgets": "^1.9.0" + "engines": { + "node": ">= 6" } }, - "@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" - }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/bent": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@types/bent/-/bent-7.3.3.tgz", + "integrity": "sha512-5NEIhVzHiZ6wMjFBmJ3gwjxwGug6amMoAn93rtDBttwrODxm+bt63u+MJA7H9NGGM4X1m73sJrAxDapktl036Q==", + "dev": true, "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" - } - } + "@types/node": "*" } }, - "@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" - } - } + "node_modules/@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "dev": true + }, + "node_modules/@types/chai-arrays": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/chai-arrays/-/chai-arrays-2.0.0.tgz", + "integrity": "sha512-5h5jnAC9C64YnD7WJpA5gBG7CppF/QmoWytOssJ6ysENllW49NBdpsTx6uuIBOpnzAnXThb8jBICgB62wezTLQ==", + "dev": true, + "dependencies": { + "@types/chai": "*" } }, - "@jupyterlab/ui-components": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/ui-components/-/ui-components-1.2.1.tgz", - "integrity": "sha512-GUtIRwTmFnlJaPUM8SiFw1STmsyMVGjchLKqIoQnn0qYAJvaSUGyRqqoSD5iIpwov6OHCOOyxH6fQ5OAtH1kwA==", + "node_modules/@types/chai-as-promised": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz", + "integrity": "sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==", "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" - } - } + "@types/chai": "*" } }, - "@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" - }, + "node_modules/@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 + }, + "node_modules/@types/decompress": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", + "integrity": "sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ==", + "dev": true, "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" - } - } + "@types/node": "*" } }, - "@mapbox/polylabel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/polylabel/-/polylabel-1.0.2.tgz", - "integrity": "sha1-xXFGGbZa3QgmOOoGAn5psUUA76Y=", + "node_modules/@types/download": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.3.tgz", + "integrity": "sha512-IDwXjU7zCtuFVvI0Plnb02TpXyj3RA4YeOKQvEfsjdJeWxZ9hTl6lxeNsU2bLWn0aeAS7fyMl74w/TbdOlS2KQ==", "dev": true, - "requires": { - "tinyqueue": "^1.1.0" + "dependencies": { + "@types/decompress": "*", + "@types/got": "^9", + "@types/node": "*" } }, - "@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" - }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, "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==" - } + "@types/estree": "*", + "@types/json-schema": "*" } }, - "@nteract/markdown": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-3.0.1.tgz", - "integrity": "sha512-Dr79niruytzbcDsLLS5NoPIzvVJ3YkJX18O0hmZAmrVn2Ni8L5VW+ZymWsSgRBogDgrYQzhV4L1MmIC/X6rbUg==", + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "@nteract/mathjax": "^3.0.1", - "babel-runtime": "^6.26.0", - "react-markdown": "^4.0.0" + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" } }, - "@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==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0" + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" } }, - "@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" - }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, "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==" - } + "@types/minimatch": "*", + "@types/node": "*" } }, - "@nteract/octicons": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@nteract/octicons/-/octicons-0.5.1.tgz", - "integrity": "sha512-7d42YfTbaiL2cF8txcQirDCNTbhmKfSoJSRlFMnEH11D0e0XUk/dxEWRGxxLrbP3SApY84hF/9uP3u1p6jqiAQ==", + "node_modules/@types/got": { + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz", + "integrity": "sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0" + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.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==", + "node_modules/@types/got/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0", - "styled-jsx": "^3.1.0" + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" } }, - "@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==", + "node_modules/@types/got/node_modules/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, - "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" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ] + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@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 }, - "@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==", + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0", - "leaflet": "^1.0.3" + "dependencies": { + "@types/node": "*" } }, - "@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==", + "node_modules/@types/lodash": { + "version": "4.14.181", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", + "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", + "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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==", + "node_modules/@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 + }, + "node_modules/@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, + "node_modules/@types/shortid": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", + "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=", + "dev": true + }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", "dev": true, - "requires": { - "lodash": "^4.17.4", - "plotly.js-dist": "^1.48.3" + "dependencies": { + "@types/sinonjs__fake-timers": "*" } }, - "@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==", + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, + "node_modules/@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 + }, + "node_modules/@types/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", + "dev": true + }, + "node_modules/@types/vscode": { + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0", - "lodash": "^4.17.4" - } + "license": "MIT" }, - "@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==", + "node_modules/@types/which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz", + "integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==", + "dev": true + }, + "node_modules/@types/winreg": { + "version": "1.2.31", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.31.tgz", + "integrity": "sha512-SDatEMEtQ1cJK3esIdH6colduWBP+42Xw9Guq1sf/N6rM3ZxgljBduvZOwBsxRps/k5+Wwf5HJun6pH8OnD2gg==", + "dev": true + }, + "node_modules/@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", "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" + "dependencies": { + "@types/node": "*" } }, - "@nteract/transforms": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@nteract/transforms/-/transforms-4.4.7.tgz", - "integrity": "sha512-G84BeBfzfdAg3cAV78ES9/yxe2ZNitg7j8MCKKx4iNS9vqeQbO92vyBsBW2D6qlddefU/RB5/HUvvtNUpd0ZRw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "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" + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "@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" - }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, "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==" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "@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==", + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, - "requires": { - "d3": "3.5.17", - "d3-cloud": "^1.2.5", - "datalib": "^1.9.1", - "json-stable-stringify": "^1.0.1" + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "@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==", + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "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" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@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==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, - "requires": { - "@phosphor/commands": "^1.7.2", - "@phosphor/coreutils": "^1.3.1", - "@phosphor/widgets": "^1.9.3" + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "@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" - }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, "dependencies": { - "@phosphor/algorithm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", - "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true } } }, - "@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" - }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, "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" - } + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "@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==", + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, - "requires": { - "@phosphor/coreutils": "^1.3.1", - "@phosphor/disposable": "^1.3.1" + "engines": { + "node": "^16.0.0 || >=18.0.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" - } - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "@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" - }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, "dependencies": { - "@phosphor/algorithm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", - "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true } } }, - "@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" + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" } }, - "@phosphor/virtualdom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/virtualdom/-/virtualdom-1.2.0.tgz", - "integrity": "sha512-L9mKNhK2XtVjzjuHLG2uYuepSz8uPyu6vhF4EgCP0rt0TiLYaZeHwuNu3XeFbul9DMOn49eBpye/tfQVd4Ks+w==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "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 + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": 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" - } - } + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "@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=", + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "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 - } + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" } }, - "@sindresorhus/is": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", - "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "node_modules/@typescript-eslint/utils/node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "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==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, - "requires": { - "type-detect": "4.0.8" + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "@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==", + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } + "license": "ISC" }, - "@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" + "node_modules/@vscode/extension-telemetry": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", + "dependencies": { + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" + }, + "engines": { + "vscode": "^1.75.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==", + "node_modules/@vscode/test-electron": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.8.tgz", + "integrity": "sha512-b4aZZsBKtMGdDljAsOPObnAi7+VWIaYl3ylCz1jTs+oV6BZ4TNHcVNC3xUn0azPeszBmwSBDQYfFESIaUQnrOg==", "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" + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=16" } }, - "@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==", + "node_modules/@vscode/vsce": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.27.0.tgz", + "integrity": "sha512-FFUMBVSyyjjJpWszwqk7d4U3YllY8FdWslbUDMRki1x4ZjA3Z0hmRMfypWrjP9sptbSR9nyPFU4uqjhy2qRB/w==", "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" - } - } + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 16" + }, + "optionalDependencies": { + "keytar": "^7.7.0" } }, - "@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==", + "node_modules/@vscode/vsce-sign": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.4.tgz", + "integrity": "sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==", + "dev": true, + "hasInstallScript": true, + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "cpu": [ + "arm64" + ], "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" - } - } - } + "optional": true, + "os": [ + "alpine" + ] }, - "@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 + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "alpine" + ] }, - "@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 + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] }, - "@types/anymatch": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", - "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", - "dev": true + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] }, - "@types/backbone": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@types/backbone/-/backbone-1.4.1.tgz", - "integrity": "sha512-KYfGuQy4d2vvYXbn0uHFZ6brFLndatTMomxBlljpbWf4kFpA3BG/6LA3ec+J9iredrX6eAVI7sm9SVAvwiIM6g==", + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "cpu": [ + "arm" + ], "dev": true, - "requires": { - "@types/jquery": "*", - "@types/underscore": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "@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==", + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "@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 + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "@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 + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, - "@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==", + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "@types/chai": "*" + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" } }, - "@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==", + "node_modules/@vscode/vsce/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, - "requires": { - "@types/chai": "*" + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" } }, - "@types/cheerio": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.12.tgz", - "integrity": "sha512-aczowyAJNfrkBV+HS8DyAA87OnvkqGrrOmm5s7V6Jbgimzv/1ZoAy91cLJX8GQrUS60KufD7EIzA2LbK8HV4hg==", + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "requires": { - "@types/node": "*" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "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==", + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, - "@types/clone": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz", - "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=", + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "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==", + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "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": "*" - } + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true }, - "@types/connect": { - "version": "3.4.32", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", - "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" } }, - "@types/cookiejar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", - "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "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==", + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/node": "*", - "@types/webpack": "*" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, - "@types/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg==", + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, - "requires": { - "@types/express": "*" + "dependencies": { + "@xtuc/ieee754": "^1.2.0" } }, - "@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==", + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "@xtuc/long": "4.2.2" } }, - "@types/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@types/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==", + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "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==", + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, - "requires": { - "@types/glob": "*" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, - "@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==", + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, - "requires": { - "@types/decompress": "*", - "@types/got": "^8", - "@types/node": "*" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, - "@types/enzyme": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.2.tgz", - "integrity": "sha512-tAFqfBVAxaxOCCcCpFrv6yFDg/hwL1KtJc7dsOaO+BBdSppTcyRP7jO2ze5va7NDa8iSFfAaZ9NeuybmMulu0w==", + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, - "requires": { - "@types/cheerio": "*", - "@types/react": "*" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, - "@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==", + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, - "requires": { - "@types/enzyme": "*" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, - "@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==", + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" } }, - "@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==", + "node_modules/@webpack-cli/configtest": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", + "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" } }, - "@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==", + "node_modules/@webpack-cli/info": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", + "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", "dev": true, - "requires": { - "@types/node": "*", - "@types/range-parser": "*" + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" } }, - "@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==", + "node_modules/@webpack-cli/serve": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", + "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", "dev": true, - "requires": { - "@types/node": "*" + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } } }, - "@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": "*" - } + "node_modules/@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 }, - "@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==", + "node_modules/@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 }, - "@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": "*" + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" } }, - "@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": "*" + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peerDependencies": { + "acorn": "^8" } }, - "@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==", + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "requires": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.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==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "requires": { - "@types/clean-css": "*", - "@types/relateurl": "*", - "@types/uglify-js": "*" + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "@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==", + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, - "requires": { - "@types/html-minifier": "*", - "@types/tapable": "*", - "@types/webpack": "*" + "engines": { + "node": ">=0.4.0" } }, - "@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": "*" + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "@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 + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": 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==", + "node_modules/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==", "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "@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==", + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "@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==", + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "requires": { - "@types/events": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^4.0.0" + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "@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 + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "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==", + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "requires": { - "@types/react": "*" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "@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==", + "node_modules/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": { - "@types/node": "*", - "@types/webpack": "*" + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "@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==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "requires": { - "@types/node": "*" + "engines": { + "node": ">=8" } }, - "@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 + "node_modules/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, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } }, - "@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 + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } }, - "@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 + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } }, - "@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==", + "node_modules/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": { - "@types/node": "*" + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "@types/node": { - "version": "10.14.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.18.tgz", - "integrity": "sha512-ryO3Q3++yZC/+b8j8BdKd/dn9JlzlHBPdm80656xwYUdmPkpTGTjkAdt6BByiNupGPE8w0FhBgvYy/fX9hRNGQ==", - "dev": true + "node_modules/append-buffer/node_modules/buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } }, - "@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==", + "node_modules/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": { - "@types/node": "*", - "form-data": "^3.0.0" + "dependencies": { + "default-require-extensions": "^3.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/applicationinsights": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", "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" - } + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", + "@microsoft/applicationinsights-web-snippet": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "applicationinsights-native-metrics": "*" + }, + "peerDependenciesMeta": { + "applicationinsights-native-metrics": { + "optional": true } } }, - "@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==", + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", "dev": true, - "requires": { - "@types/node": "*" + "license": "MIT", + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" } }, - "@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 + "node_modules/archive-type/node_modules/file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "@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==", + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "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==", + "node_modules/arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", "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==", + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "requires": { - "@types/prop-types": "*", - "csstype": "^2.2.0" + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "@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==", + "node_modules/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, - "requires": { - "@types/react": "*" + "engines": { + "node": ">=0.10.0" } }, - "@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==", + "node_modules/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, - "requires": { - "@types/react": "*" + "engines": { + "node": ">=0.10.0" } }, - "@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==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@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==", + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/react": "*" + "engines": { + "node": ">=0.10.0" } }, - "@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==", + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "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 - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@types/relateurl": { - "version": "0.2.28", - "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz", - "integrity": "sha1-a9p9uGU/piZD9e5p6facEaOS46Y=", - "dev": true + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } }, - "@types/request": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "requires": { - "@types/caseless": "*", - "@types/form-data": "*", - "@types/node": "*", - "@types/tough-cookie": "*" + "engines": { + "node": ">=8" } }, - "@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 + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "@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==", + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, - "requires": { - "@types/express-serve-static-core": "*", - "@types/mime": "*" + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@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==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@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 + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "@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 + "node_modules/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, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } }, - "@types/superagent": { - "version": "3.8.7", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", - "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", + "node_modules/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": { - "@types/cookiejar": "*", - "@types/node": "*" + "dependencies": { + "object-assign": "^4.1.1", + "util": "0.10.3" } }, - "@types/tapable": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz", - "integrity": "sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==", + "node_modules/assert/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", "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==", + "node_modules/assert/node_modules/util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "inherits": "2.0.1" } }, - "@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==", + "node_modules/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, - "requires": { - "pretty-format": "^24.3.0" + "engines": { + "node": "*" } }, - "@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==", + "node_modules/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, - "requires": { - "@types/react-dom": "*", - "@types/testing-library__dom": "*" + "engines": { + "node": ">=0.10.0" } }, - "@types/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "node_modules/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 }, - "@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==", + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "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==", + "node_modules/async-done": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "dev": true, - "requires": { - "source-map": "^0.6.1" + "dependencies": { + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" } }, - "@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 + "node_modules/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==", + "dependencies": { + "stack-chain": "^1.3.7" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3" + } }, - "@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": "*" + "node_modules/async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "dependencies": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + }, + "engines": { + "node": "<=0.11.8 || >0.11.10" } }, - "@types/vscode": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.47.0.tgz", - "integrity": "sha512-nJA37ykkz9FYA0ZOQUSc3OZnhuzEW2vUhUEo4MiduUo82jGwwcLfyvmgd/Q7b0WrZAAceojGhZybg319L24bTA==", - "dev": true + "node_modules/async-listener/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } }, - "@types/webpack": { - "version": "4.4.34", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.4.34.tgz", - "integrity": "sha512-GnEBgjHsfO1M7DIQ0dAupSofcmDItE3Zsu3reK8SQpl/6N0rtUQxUmQzVFAS5ou/FGjsYKjXAWfItLZ0kNFTfQ==", + "node_modules/async-settle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, - "requires": { - "@types/anymatch": "*", - "@types/node": "*", - "@types/tapable": "*", - "@types/uglify-js": "*", - "source-map": "^0.6.0" + "dependencies": { + "async-done": "^2.0.0" + }, + "engines": { + "node": ">= 10.13.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==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "requires": { - "@types/webpack": "*" + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@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==", + "node_modules/axe-core": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz", + "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", "dev": true, - "requires": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.6.1" + "engines": { + "node": ">=4" } }, - "@types/winreg": { - "version": "1.2.30", - "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", - "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg=", + "node_modules/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 }, - "@types/ws": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", - "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, - "requires": { - "@types/events": "*", - "@types/node": "*" + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" } }, - "@types/xml2js": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.4.tgz", - "integrity": "sha512-O6Xgai01b9PB3IGA0lRIp1Ex3JBcxGDhdO0n3NIIpCyDOAjxcIGQFmkvgJpP8anTrthxOUQjBfLdRRi0Zn/TXA==", + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, + "node_modules/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=", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, - "@types/yargs": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.5.tgz", - "integrity": "sha512-CF/+sxTO7FOwbIRL4wMv0ZYLCRfMid2HQpzDRyViH7kSpfoAFiMdGqKIxb1PxWfjtQXQhnQuD33lvRHNwr809Q==", + "node_modules/babel-runtime/node_modules/core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", "dev": true, - "requires": { - "@types/yargs-parser": "*" - } + "hasInstallScript": true }, - "@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==", + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "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==", + "node_modules/bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, - "optional": true, - "requires": { - "@types/node": "*" + "dependencies": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "@typescript-eslint/eslint-plugin": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.7.0.tgz", - "integrity": "sha512-4OEcPON3QIx0ntsuiuFP/TkldmBGXf0uKxPQlGtS/W2F3ndYm8Vgdpj/woPJkzUc65gd3iR+qi3K8SDQP/obFg==", + "node_modules/bach/node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "3.7.0", - "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" - } + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "dev": true, + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "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" - } + { + "type": "consulting", + "url": "https://feross.org/support" } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, - "@typescript-eslint/experimental-utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.0.tgz", - "integrity": "sha512-xpfXXAfZqhhqs5RPQBfAFrWDHoNxD5+sVB5A46TF58Bq1hRfVROrWHcQHHUM9aCBdy9+cwATcvCbRg8aIRbaHQ==", + "node_modules/bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.7.0", - "@typescript-eslint/typescript-estree": "3.7.0", - "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" - } - } + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" } }, - "@typescript-eslint/parser": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.7.0.tgz", - "integrity": "sha512-2LZauVUt7jAWkcIW7djUc3kyW+fSarNEuM3RF2JdLHR9BfX/nDEnyA4/uWz0wseoWVZbDXDF7iF9Jc342flNqQ==", + "node_modules/bent/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.7.0", - "@typescript-eslint/types": "3.7.0", - "@typescript-eslint/typescript-estree": "3.7.0", - "eslint-visitor-keys": "^1.1.0" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@typescript-eslint/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.7.0.tgz", - "integrity": "sha512-reCaK+hyKkKF+itoylAnLzFeNYAEktB0XVfSQvf0gcVgpz1l49Lt6Vo9x4MVCCxiDydA0iLAjTF/ODH0pbfnpg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.0.tgz", - "integrity": "sha512-xr5oobkYRebejlACGr1TJ0Z/r0a2/HUf0SXqPvlgUMwiMqOCu/J+/Dr9U3T0IxpE5oLFSkqMx1FE/dKaZ8KsOQ==", + "node_modules/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, - "requires": { - "@typescript-eslint/types": "3.7.0", - "@typescript-eslint/visitor-keys": "3.7.0", - "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 - }, - "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" - } - } + "engines": { + "node": "*" } }, - "@typescript-eslint/visitor-keys": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.0.tgz", - "integrity": "sha512-k5PiZdB4vklUpUX4NBncn5RBKty8G3ihTY+hqJsCdMuD0v4jofI5xuqwnVcWxfv6iTm2P/dfEa2wMUnsUY8ODw==", + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" + "engines": { + "node": ">=8" } }, - "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, - "@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 + "node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" }, - "@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==", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "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 + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "@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==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.8.5" + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "@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==", + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "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==", + "node_modules/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 }, - "@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==", + "node_modules/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": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" + "dependencies": { + "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" } }, - "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "node_modules/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": { - "@xtuc/ieee754": "^1.2.0" + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" } }, - "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "node_modules/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": { - "@xtuc/long": "4.2.2" + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.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==", + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "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" + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, - "@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==", + "node_modules/browserify-rsa/node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "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" - } + "license": "MIT" }, - "@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==", + "node_modules/browserify-rsa/node_modules/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, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "@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==", + "node_modules/browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "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" + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, - "@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==", + "node_modules/browserify-sign/node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "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" - } + "license": "MIT" }, - "@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==", + "node_modules/browserify-sign/node_modules/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, - "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 + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "node_modules/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": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "dependencies": { + "pako": "~1.0.5" } }, - "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==", + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "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 + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - } - }, - "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==" - } + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "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==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, - "requires": { - "es6-promisify": "^5.0.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, - "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" + "node_modules/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, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.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==", + "node_modules/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, - "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" - } + "license": "MIT" }, - "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" + "node_modules/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, + "engines": { + "node": "*" } }, - "ajv-errors": { + "node_modules/buffer-equal-constant-time": { "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==", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "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==", + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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 }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "optional": true + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "node_modules/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 }, - "anser": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.8.tgz", - "integrity": "sha512-tVHucTCKIt9VRrpQKzPtOlwm/3AmyQ7J+QE29ixFnvuE2hm83utEVrN7jJapYkHV6hI0HOHkEX9TOMCzHtwvuA==", + "node_modules/bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==", "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==", + "node_modules/cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" + "license": "MIT", + "dependencies": { + "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" } }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true, - "requires": { - "ansi-wrap": "0.1.0" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "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 + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "node_modules/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": { - "ansi-wrap": "0.1.0" + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, - "requires": { - "ansi-wrap": "0.1.0" + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "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==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, - "requires": { - "color-convert": "^1.9.0" + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "requires": { - "entities": "^1.1.1" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "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==", + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "anser": "^1.4.1", - "babel-runtime": "^6.26.0", - "escape-carriage": "^1.2.0" + "engines": { + "node": ">=6" } }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "node_modules/caniuse-lite": { + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", "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" - } - } + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" } }, - "append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "node_modules/chai-arrays": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chai-arrays/-/chai-arrays-2.2.0.tgz", + "integrity": "sha512-4awrdGI2EH8owJ9I58PXwG4N56/FiM8bsn4CVSNEgr4GKAM6Kq5JPVApUbhUBjDakbZNuRvV7quRSC38PWq/tg==", "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 - } + "engines": { + "node": ">=0.10" } }, - "append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "node_modules/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": { - "default-require-extensions": "^3.0.0" + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" } }, - "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" + "node_modules/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, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true, + "engines": { + "node": "*" + } }, - "arch": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", - "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } }, - "archive-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", "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 - } + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" } }, - "archiver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.0.0.tgz", - "integrity": "sha512-5QeR6Xc5hSA9X1rbQfcuQ6VZuUXOaEdB65Dhmk9duuRJHYif/ZyJfuyJqsQrj34PFjU5emv5/MmfgA8un06onw==", + "node_modules/cheerio-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", "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": { + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, "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" - } - } + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "archiver-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.0.0.tgz", - "integrity": "sha512-JRBgcVvDX4Mwu2RBF8bBaHcQCSxab7afsxAPYDQ5W+19quIPP5CfKE7Ql+UHs9wYvwsaNR8oDuhtf5iqrKmzww==", + "node_modules/cheerio-select/node_modules/css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", "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" + "engines": { + "node": ">= 6" }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, "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" - } - } + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true + "node_modules/cheerio-select/node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] }, - "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==", + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "dependencies": { + "domelementtype": "^2.2.0" }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "dev": true, "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" - } - } + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "arg": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", - "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", - "dev": true + "node_modules/cheerio-select/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", "dev": true, - "requires": { - "sprintf-js": "~1.0.2" + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true + "node_modules/cheerio/node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] }, - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "node_modules/cheerio/node_modules/domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "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=", + "node_modules/cheerio/node_modules/domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", "dev": true, - "requires": { - "make-iterator": "^1.0.0" + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "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 + "node_modules/cheerio/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "dev": true, - "requires": { - "make-iterator": "^1.0.0" + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.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=", + "node_modules/cheerio/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "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=", + "node_modules/cheerio/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", "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 + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } }, - "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=" + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "optional": true }, - "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==", + "node_modules/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": { - "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 - } + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" } }, - "array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, - "requires": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, + "license": "MIT", "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 - } + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" } }, - "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==", + "node_modules/cipher-base/node_modules/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, - "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 + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } - } - }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true + ], + "license": "MIT" }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "node_modules/circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "deprecated": "CircularJSON is in maintenance only, flatted is its successor.", "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 + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, - "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==", + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "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 - } + "engines": { + "node": ">=6" } }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "requires": { - "array-uniq": "^1.0.1" + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "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==", + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.13.0" + "engines": { + "node": ">=0.8" } }, - "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==", + "node_modules/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, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", - "function-bind": "^1.1.1" + "engines": { + "node": ">= 0.10" } }, - "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==", + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "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.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 - } + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "array.prototype.map": { + "node_modules/clone-response": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", - "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "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" - }, + "license": "MIT", "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 - } + "mimic-response": "^1.0.0" } }, - "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==", + "node_modules/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 }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true + "node_modules/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, + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true + "node_modules/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==", + "dependencies": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" + } }, - "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" + "node_modules/cls-hooked/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" } }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "node_modules/cockatiel": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.3.tgz", + "integrity": "sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==", "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "engines": { + "node": ">=16" } }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "node_modules/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": { - "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" - } - } + "color-name": "1.1.3" } }, - "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==", + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "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=", + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", "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" - }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "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" - } - } + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "ast-types": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz", - "integrity": "sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk=" + "node_modules/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 }, - "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=", + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/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 + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/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 }, - "astral-regex": { + "node_modules/constants-browserify": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", "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" + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "node_modules/content-disposition/node_modules/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, - "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 + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "license": "MIT" + }, + "node_modules/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==", + "dependencies": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" } }, - "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==", + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "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" + "node_modules/copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dev": true, + "dependencies": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "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==" + "node_modules/copy-props/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "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" + "node_modules/copy-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" } }, - "async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "requires": { - "async-done": "^1.2.2" + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "node_modules/core-js-pure": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "node_modules/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 }, - "autoprefixer": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.6.tgz", - "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==", + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "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" - }, + "license": "MIT", "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" - } - } + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" } }, - "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=" + "node_modules/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, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "node_modules/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, + "dependencies": { + "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" + } }, - "axe-core": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.5.tgz", - "integrity": "sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==", + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "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" + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" } }, - "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 + "node_modules/cross-env/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } }, - "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==", + "node_modules/cross-env/node_modules/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, - "requires": { - "os": "0.1.1", - "tunnel": "0.0.4", - "typed-rest-client": "1.2.0", - "underscore": "1.8.3" + "engines": { + "node": ">=8" } }, - "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" - }, + "node_modules/cross-env/node_modules/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, "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" - } - } + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "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=", + "node_modules/cross-env/node_modules/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, - "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 - } + "engines": { + "node": ">=8" } }, - "babel-loader": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", - "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, - "requires": { - "find-cache-dir": "^2.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "pify": "^4.0.1" + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" } }, - "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==", + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "requires": { - "object.assign": "^4.1.0" + "bin": { + "semver": "bin/semver" } }, - "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==", + "node_modules/cross-spawn/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "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 - } + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "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==", + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", "dev": true, - "requires": { - "decache": "^4.5.1" + "engines": { + "node": "*" } }, - "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==", + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", "dev": true, - "requires": { - "@babel/runtime": "^7.4.2", - "cosmiconfig": "^5.2.0", - "resolve": "^1.10.0" + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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=", + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "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=", + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", "dev": true, - "requires": { - "babel-runtime": "^6.22.0" + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, "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 - } + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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" - }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "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==" - } + "ms": "2.0.0" } }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "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 - } + "engines": { + "node": ">=0.10.0" } }, - "bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "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" + "license": "MIT", + "engines": { + "node": ">=0.10" } }, - "backbone": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.2.3.tgz", - "integrity": "sha1-wiz9B/yG676uYdGJKe0RXpmdZbk=", + "node_modules/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": { - "underscore": ">=1.7.0" + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=4" } }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", - "dev": true + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } }, - "bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "dev": true + "node_modules/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, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "node_modules/decompress-tar/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "node_modules/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": { - "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" + "license": "MIT", + "dependencies": { + "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" }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/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, + "license": "MIT", "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" - } - } + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" } }, - "base16": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", - "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=", - "dev": true + "node_modules/decompress-targz/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } }, - "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==" + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "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 + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "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" + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "node_modules/decompress/node_modules/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": { - "callsite": "1.0.0" + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "bfj": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", - "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, - "requires": { - "bluebird": "^3.5.5", - "check-types": "^8.0.3", - "hoopy": "^0.1.4", - "tryer": "^1.0.1" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "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 + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true + "node_modules/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, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "node_modules/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, "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" + "engines": { + "node": ">=4.0.0" } }, - "bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=", + "node_modules/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 }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "node_modules/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": { - "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" - } - } + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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=", + "node_modules/default-require-extensions/node_modules/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, - "requires": { - "inherits": "~2.0.0" + "engines": { + "node": ">=8" } }, - "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 + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "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 + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "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 - } + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "bootstrap": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", - "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==", - "dev": true + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } }, - "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 + "node_modules/des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } }, - "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" + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "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" - } - } + "optional": true, + "engines": { + "node": ">=8" } }, - "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" - }, + "node_modules/diagnostic-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "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" - } - } + "semver": "^7.5.3" } }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "node_modules/diagnostic-channel-publishers": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", + "peerDependencies": { + "diagnostic-channel": "*" + } }, - "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" + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, - "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 + "node_modules/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, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } }, - "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" - }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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 + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } }, - "browserify-aes": { + "node_modules/domain-browser": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "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" + "engines": { + "node": ">=0.4", + "npm": ">=1.2" } }, - "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==", + "node_modules/download": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/download/-/download-8.0.0.tgz", + "integrity": "sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==", "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" + "license": "MIT", + "dependencies": { + "archive-type": "^4.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.2.1", + "ext-name": "^5.0.0", + "file-type": "^11.1.0", + "filenamify": "^3.0.0", + "get-stream": "^4.1.0", + "got": "^8.3.1", + "make-dir": "^2.1.0", + "p-event": "^2.1.0", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=10" } }, - "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==", + "node_modules/download/node_modules/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": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" } }, - "browserify-mime": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", - "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" + "node_modules/download/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } }, - "browserify-optional": { + "node_modules/dunder-proto": { "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" + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } + "license": "BSD-3-Clause" }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "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", + "dependencies": { + "end-of-stream": "^1.0.0", "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "readable-stream": "^2.0.0", + "stream-shift": "^1.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==", + "node_modules/each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "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 - } + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "browserslist": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.4.tgz", - "integrity": "sha512-ErJT8qGfRt/VWHSr1HeqZzz50DvxHtr1fVL1m5wf20aGrG8e1ce8fpZ2EjZEfs09DDZYSvtRaDlMpWslBf8Low==", + "node_modules/each-props/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000981", - "electron-to-chromium": "^1.3.188", - "node-releases": "^1.1.25" + "engines": { + "node": ">=0.10.0" } }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } + "license": "MIT" }, - "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==", + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" + "dependencies": { + "safe-buffer": "^5.0.1" } }, - "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==", + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "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 + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } }, - "buffer-equal": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", - "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" + "node_modules/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==", + "dependencies": { + "shimmer": "^1.2.0" + } }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "node_modules/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 }, - "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==" + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } }, - "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 + "node_modules/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, + "dependencies": { + "once": "^1.4.0" + } }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } }, - "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 + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, - "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 + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "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" - }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "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 - } + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" } }, - "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" + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "cache-loader": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-4.1.0.tgz", - "integrity": "sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==", + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "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 - } + "hasown": "^2.0.0" } }, - "cacheable-request": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "node_modules/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": { - "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 - } + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/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 + }, + "node_modules/es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "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 - } + "engines": { + "node": ">=6" } }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "node_modules/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, - "requires": { - "callsites": "^2.0.0" + "engines": { + "node": ">=0.8.0" } }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "requires": { - "caller-callsite": "^2.0.0" + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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=", + "node_modules/eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" } }, - "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=", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "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" - } - } + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, - "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==", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "requires": { - "nan": "^2.14.0", - "node-pre-gyp": "^0.11.0", - "simple-get": "^3.0.3" + "dependencies": { + "ms": "^2.1.1" } }, - "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==", + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, - "requires": { - "get-proxy": "^2.0.0", - "isurl": "^1.0.0-alpha5", - "tunnel-agent": "^0.6.0", - "url-to-options": "^1.0.1" + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "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" + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" } }, - "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 + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } }, - "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==", + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "requires": { - "check-error": "^1.0.2" + "dependencies": { + "ms": "^2.1.1" } }, - "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==", + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "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" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "bin": { + "semver": "bin/semver.js" } }, - "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==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", + "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", "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": "*" - } - } + "@babel/runtime": "^7.16.3", + "aria-query": "^4.2.2", + "array-includes": "^3.1.4", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.3.5", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.7", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.2.1", + "language-tags": "^1.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/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": { - "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" + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" } }, - "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "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==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "requires": { - "tslib": "^1.9.0" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "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==", + "node_modules/eslint-plugin-no-only-tests": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", + "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "license": "MIT", + "engines": { + "node": ">=5.0.0" } }, - "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==", + "node_modules/eslint-plugin-react": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", + "integrity": "sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==", "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" - } - } + "array-includes": "^3.1.4", + "array.prototype.flatmap": "^1.2.5", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.0", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "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==", + "node_modules/eslint-plugin-react-hooks": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz", + "integrity": "sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ==", "dev": true, - "requires": { - "source-map": "~0.6.0" + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-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=", + "node_modules/eslint-plugin-react/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "requires": { - "restore-cursor": "^2.0.0" + "engines": { + "node": ">=4.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=", + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, "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" - } - } + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } }, - "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 + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } }, - "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==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "requires": { - "for-own": "^1.0.0", - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.0", - "shallow-clone": "^1.0.0" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.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" - } - } + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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" + "node_modules/eslint/node_modules/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, + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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 + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" }, - "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==", + "node_modules/eslint/node_modules/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": { - "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" - } - } + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "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" + "node_modules/eslint/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "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=", + "node_modules/eslint/node_modules/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 }, - "codecov": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.1.tgz", - "integrity": "sha512-JHWxyPTkMLLJn9SmKJnwAnvY09kg2Os2+Ux+GG7LwZ9g8gzDDISpIN5wAsH1UBaafA/yGcd3KofMaorE8qd6Lw==", + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "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" + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "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" - } + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "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 + "node_modules/eslint/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } }, - "collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "requires": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "engines": { + "node": ">=10" }, - "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" - } - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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" + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.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==", - "requires": { - "color-name": "1.1.3" + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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" + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "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" + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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" + "node_modules/eslint/node_modules/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, + "engines": { + "node": ">=8" } }, - "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 + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "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 + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "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 + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "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 + "node_modules/eslint/node_modules/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, + "engines": { + "node": ">=8" + } }, - "compress-commons": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", - "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", + "node_modules/eslint/node_modules/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": { - "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" - } - } + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "node_modules/eslint/node_modules/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, + "engines": { + "node": ">=8" + } }, - "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" - }, + "node_modules/eslint/node_modules/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, "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" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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=", + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "requires": { - "date-now": "^0.1.4" + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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 + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } }, - "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 + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "requires": { - "safe-buffer": "5.1.2" + "engines": { + "node": ">=4.0" } }, - "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 + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } }, - "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" + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "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" + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" } }, - "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 + "node_modules/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, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true + "node_modules/execa/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "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" + "engines": { + "node": ">=10" }, - "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" - } - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true + "node_modules/execa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "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==", + "node_modules/execa/node_modules/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, - "requires": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/execa/node_modules/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": { - "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" - } - }, - "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" - } - } + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "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 + "node_modules/execa/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } }, - "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==", + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "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 - } + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "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=" + "node_modules/expose-loader": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", + "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "node_modules/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": { - "object-assign": "^4", - "vary": "^1" + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "node_modules/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": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "cp-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", - "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/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": { - "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" + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "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==", + "node_modules/extend-shallow/node_modules/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": { - "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": { + "is-plain-object": "^2.0.4" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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 + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, "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" - } - } + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" } }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "requires": { - "buffer": "^5.1.0" + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "crc32-stream": { + "node_modules/fast-json-stable-stringify": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", - "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "node_modules/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 + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "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" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" }, - "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" - } + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" } - } + ] }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "node_modules/fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "dependencies": { + "reusify": "^1.0.4" } }, - "create-emotion": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz", - "integrity": "sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==", + "node_modules/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": { - "@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" + "dependencies": { + "pend": "~1.2.0" } }, - "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==", + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "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" + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.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==", + "node_modules/file-type": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", + "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==", "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" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "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==", + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true, - "requires": { - "gud": "^1.0.0", - "warning": "^4.0.3" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/filenamify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-3.0.0.tgz", + "integrity": "sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g==", "dev": true, - "requires": { - "cross-spawn": "^7.0.0" - }, + "license": "MIT", "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" - } - } + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=6" } }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "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" + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "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=", + "node_modules/filter-obj": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz", + "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", "dev": true, - "requires": { - "lru-cache": "^4.0.0", - "which": "^1.2.8" + "engines": { + "node": ">=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==", + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "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" + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, - "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==", + "node_modules/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": { - "inherits": "^2.0.3", - "source-map": "^0.6.1", - "source-map-resolve": "^0.5.2", - "urix": "^0.1.0" + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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=", + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "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" - } - } + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" } }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "node_modules/fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "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" - } - } + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" } }, - "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==", + "node_modules/fined/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "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" - } - } + "engines": { + "node": ">=0.10.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==", + "node_modules/flagged-respawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true, - "requires": { - "cssom": "~0.3.6" + "engines": { + "node": ">= 10.13.0" } }, - "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" + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" } }, - "d3": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", - "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=", - "dev": true + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } }, - "d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "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==", + "node_modules/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": { - "d3-quadtree": "1.0.1" + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, - "d3-brush": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz", - "integrity": "sha512-lGSiF5SoSqO5/mYGD5FAeGKKS62JdA1EV7HPrU2b5rTX4qEJJtpjaGLJngjnkewQy7UnGstnFd3168wpf5z76w==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "requires": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true, - "requires": { - "d3-array": "1", - "d3-path": "1" + "engines": { + "node": ">=0.10.0" } }, - "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==", + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", "dev": true, - "requires": { - "d3-dispatch": "^1.0.3" + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "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==", + "node_modules/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": { - "d3-array": "^1.1.1" + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" } }, - "d3-delaunay": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.1.6.tgz", - "integrity": "sha512-VF6bxon2bn1cdXuesInEtVKlE4aUfq5IjE5y0Jl2aZP1yvLsf0QENqQxNhjS4vq95EmYKauA30ofTwvREtPSXA==", + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "requires": { - "delaunator": "4" + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "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==", + "node_modules/foreground-child/node_modules/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, - "requires": { - "d3-dispatch": "1", - "d3-selection": "1" + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/foreground-child/node_modules/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": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/foreground-child/node_modules/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, - "requires": { - "d3-array": "1" + "engines": { + "node": ">=8" } }, - "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 + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } }, - "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==", + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, - "requires": { - "d3-color": "1" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" } }, - "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==", + "node_modules/fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", "dev": true }, - "d3-quadtree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.1.tgz", - "integrity": "sha1-E74CViTxEEBe1DU2xQaq7Bme1ZE=", + "node_modules/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 }, - "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" + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "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" - } - } + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "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" + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.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==", + "node_modules/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": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" } }, - "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==", + "node_modules/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": { - "d3-color": "1", - "d3-interpolate": "1" + "dependencies": { + "async": "*" } }, - "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 + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "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==", + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "requires": { - "d3-path": "1" + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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 + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "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==", + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, - "requires": { - "d3-time": "1" + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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 + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "d3-transition": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", - "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "requires": { - "d3-color": "1", - "d3-dispatch": "1", - "d3-ease": "1", - "d3-interpolate": "1", - "d3-selection": "^1.1.0", - "d3-timer": "1" + "engines": { + "node": ">=6.9.0" } }, - "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 + "node_modules/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, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "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 + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } }, - "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" + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" + "engines": { + "node": ">=8.0.0" } }, - "datalib": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/datalib/-/datalib-1.9.2.tgz", - "integrity": "sha512-sV49o/1J3VdtTlJpsvPYT39WfUxyZGTO2rEGhEJt2eNY7LN2Z9K7nq3fOjgYMQtbuL0dVCWvmtxT2hpCgwx9Mg==", + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "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" + "engines": { + "node": ">=8" }, - "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" - } - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==" + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "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 + "node_modules/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, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, - "debounce": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", - "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==", - "dev": true + "node_modules/get-stream/node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "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 - } + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "debug-fabulous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", - "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, - "requires": { - "debug": "3.X", - "memoizee": "0.4.X", - "object-assign": "4.X" + "optional": true + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, "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" - } - } + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" } }, - "decache": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/decache/-/decache-4.5.1.tgz", - "integrity": "sha512-5J37nATc6FmOTLbcsr9qx7Nm28qQyg1SK4xyEHqM0IBkNhWFp0Sm+vKoWYHD8wq+OUEb9jLyaKFfzzd1A9hcoA==", + "node_modules/glob-parent/node_modules/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": { - "callsite": "^1.0.0" + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } }, - "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=", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "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==", + "node_modules/glob-watcher": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "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 - } + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + }, + "engines": { + "node": ">= 10.13.0" } }, - "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" + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "node_modules/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": { - "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 - } + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "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 - } + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" } }, - "decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "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 - } + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "node_modules/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, - "requires": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, "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 - } + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/glogg": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", "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" - } - } + "sparkles": "^2.1.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "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" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/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": { - "type-detect": "^4.0.0" + "license": "MIT", + "dependencies": { + "@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" + }, + "engines": { + "node": ">=4" } }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + "node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "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 + "node_modules/got/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "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=" + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "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==", + "node_modules/gulp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", "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 - } + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" } }, - "default-require-extensions": { + "node_modules/gulp-cli": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", - "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", "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 - } + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" } }, - "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==", + "node_modules/gulp-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "object-keys": "^1.0.12" + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "node_modules/gulp-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "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" - } - } + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "node_modules/gulp-cli/node_modules/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": { - "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" - } - } + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "delaunator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", - "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==", + "node_modules/gulp-cli/node_modules/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": "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 + "node_modules/gulp-cli/node_modules/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, + "engines": { + "node": ">=8" + } }, - "denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", - "dev": true + "node_modules/gulp-cli/node_modules/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "node_modules/gulp-cli/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "node_modules/gulp-cli/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true + "node_modules/gulp-typescript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", + "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^3.0.5", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.3" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev" + } }, - "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 + "node_modules/gulp-typescript/node_modules/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, + "engines": { + "node": ">=6" + } }, - "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==" + "node_modules/gulp-typescript/node_modules/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, + "engines": { + "node": ">= 8" + } }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true + "node_modules/gulp-typescript/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "node_modules/gulp/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "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==", + "node_modules/gulp/node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, - "requires": { - "address": "^1.0.1", - "debug": "^2.6.0" + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.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" + "node_modules/gulp/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.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" + "node_modules/gulp/node_modules/glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" } }, - "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==", + "node_modules/gulp/node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" + "engines": { + "node": ">=10.13.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==", + "node_modules/gulp/node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, - "requires": { - "path-type": "^3.0.0" + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.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" + "node_modules/gulp/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" } }, - "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" + "node_modules/gulp/node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "doctrine": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", - "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", + "node_modules/gulp/node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "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 - } + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" } }, - "dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "node_modules/gulp/node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true, - "requires": { - "utila": "~0.4" + "engines": { + "node": ">= 10.13.0" } }, - "dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "node_modules/gulp/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, - "requires": { - "@babel/runtime": "^7.1.2" + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" } }, - "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "node_modules/gulp/node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", "dev": true, - "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "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 + "node_modules/gulp/node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } }, - "dom4": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/dom4/-/dom4-2.1.5.tgz", - "integrity": "sha512-gJbnVGq5zaBUY0lUh0LUEVGYrtN75Ks8ZwpwOYvnVFrKy/qzXK4R/1WuLIFExWj/tBxbRAkTzZUGJHXmqsBNjQ==", - "dev": true + "node_modules/gulplog": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", + "dev": true, + "dependencies": { + "glogg": "^2.2.0" + }, + "engines": { + "node": ">= 10.13.0" + } }, - "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 + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true, - "requires": { - "domelementtype": "1" + "engines": { + "node": ">=4" } }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "download": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", - "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "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" + "engines": { + "node": ">= 0.4" }, - "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 - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true + "node_modules/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, + "license": "MIT", + "engines": { + "node": "*" + } }, - "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" + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", "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" - } - } + "has-symbol-support-x": "^1.4.1" + }, + "engines": { + "node": "*" } }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "node_modules/hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "dev": true, - "requires": { - "end-of-stream": "^1.0.0", + "dependencies": { "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" + "safe-buffer": "^5.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" - } - } + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/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": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, - "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" + "node_modules/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, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" } }, - "editorconfig": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "node_modules/hasha/node_modules/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, - "requires": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" + "engines": { + "node": ">=8" } }, - "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.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", - "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", - "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" + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "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" + "node_modules/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, + "bin": { + "he": "bin/he" } }, - "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==", + "node_modules/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": { - "babel-plugin-emotion": "^9.2.11", - "create-emotion": "^9.2.12" + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, - "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" + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "node_modules/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, - "requires": { - "iconv-lite": "~0.4.13" - } + "license": "BSD-2-Clause" }, - "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" + "node_modules/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, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "engine.io": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.0.tgz", - "integrity": "sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==", + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "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 + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": 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" + "node_modules/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 + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "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" - } + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "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==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "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" + "engines": { + "node": ">=10.17.0" } }, - "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" + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "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==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "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 + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ] }, - "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==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "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" + "engines": { + "node": ">= 4" } }, - "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==", + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "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" + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "node_modules/import-fresh/node_modules/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, - "requires": { - "prr": "~1.0.1" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "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" - }, + "node_modules/import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", "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 - } + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" } }, - "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==", + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "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" + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "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.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 - }, - "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.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 - } + "engines": { + "node": ">=0.8.19" } }, - "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==", + "node_modules/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==", "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "engines": { + "node": ">=8" } }, - "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" + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" } }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", "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" + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" } }, - "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" + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" } }, - "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=", + "node_modules/into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", "dev": true, - "requires": { - "es6-promise": "^4.0.3" + "license": "MIT", + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" } }, - "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" - } + "node_modules/inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" }, - "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" + "node_modules/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, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "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==", + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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=", + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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 }, - "escape-string-regexp": { + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "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" + "node_modules/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, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "eslint": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.2.0.tgz", - "integrity": "sha512-B3BtEyaDKC5MlfDa2Ha8/D6DsS4fju95zs0hjS3HdGazw+LNayai38A25qMppK37wWGWNYSPOR6oYzlz5MHsRQ==", + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "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", - "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", - "inquirer": "^7.0.0", - "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" + "engines": { + "node": ">=10" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "dependencies": { - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "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": "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" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "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" - } - }, - "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 - }, - "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" - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "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" - } - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - } - }, - "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 - }, - "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" - } - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.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 - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "rxjs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", - "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", - "dev": true, - "requires": { - "tslib": "^1.9.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 - }, - "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 - }, - "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" - } - }, - "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.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" - } - }, - "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" - } - }, - "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" - } - } + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/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, - "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" - } - } + "engines": { + "node": ">=0.10.0" } }, - "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==", + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "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" - } - } + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "eslint-config-prettier": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.9.0.tgz", - "integrity": "sha512-k4E14HBtcLv0uqThaI6I/n1LEqROp8XaPu6SO9Z32u5NlGRC07Enu1Bh2KEFw4FNHbekH8yzbIU9kUGxbiGmCA==", + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true, - "requires": { - "get-stdin": "^6.0.0" + "engines": { + "node": ">=0.10.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==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "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" - } - } + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/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": { - "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" - } - } + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "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" - }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, "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" - } - } + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" } }, - "eslint-plugin-jsx-a11y": { + "node_modules/istanbul-lib-instrument/node_modules/semver": { "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==", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "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 - } + "bin": { + "semver": "bin/semver.js" } }, - "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==", + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" } }, - "eslint-plugin-react": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.3.tgz", - "integrity": "sha512-txbo090buDeyV0ugF3YMWrzLIUqpYTsWSDZV9xLSmExE1P/Kmgg9++PD931r+KEWS66O1c9R4srLVVHmeHpoAg==", + "node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "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" + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/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, "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.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" - } - }, - "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" - } - } + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/istanbul-lib-processinfo/node_modules/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, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" + "engines": { + "node": ">=8" } }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/istanbul-lib-processinfo/node_modules/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": { - "eslint-visitor-keys": "^1.1.0" + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "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.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz", - "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==", + "node_modules/istanbul-lib-processinfo/node_modules/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, - "requires": { - "acorn": "^7.3.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "acorn": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", - "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", - "dev": true - } + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/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": { - "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 - } + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" } }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "node_modules/istanbul-lib-report/node_modules/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, - "requires": { - "estraverse": "^4.1.0" + "engines": { + "node": ">=8" } }, - "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" + "node_modules/istanbul-lib-report/node_modules/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "node_modules/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": { - "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" + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" } }, - "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=", + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "requires": { - "original": ">=0.0.5" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": 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==", + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "execa": { + "node_modules/isurl": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "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" - }, + "license": "MIT", "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" - } - } + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" } }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "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" - }, + "license": "BlueOak-1.0.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" - } - } + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.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=", + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "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 - } + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "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==", + "node_modules/jest-worker/node_modules/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, - "requires": { - "mime-db": "^1.28.0" + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "requires": { - "ext-list": "^2.0.0", - "sort-keys-length": "^1.0.0" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "node_modules/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 }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "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" - } - } + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "node_modules/js-yaml/node_modules/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, - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, - "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" - } - } + "engines": { + "node": ">=4" } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "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" + "bin": { + "jsesc": "bin/jsesc" }, - "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=" - } + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", "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=" + "license": "MIT" }, - "fast-plist": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.2.tgz", - "integrity": "sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg=", + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "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==", + "node_modules/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 }, - "fastparse": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", - "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "node_modules/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 }, - "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==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "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" + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" } }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "dev": true, - "requires": { - "pend": "~1.2.0" + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "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=", + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" } }, - "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==", + "node_modules/jsx-ast-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", + "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", "dev": true, - "requires": { - "flat-cache": "^2.0.1" + "dependencies": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + }, + "engines": { + "node": ">=4.0" } }, - "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==", + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "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" - } - } + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "file-type": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", - "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "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==", + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, - "optional": true + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } }, - "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==", + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "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" - } - } + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "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 + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } }, - "filenamify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", - "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "node_modules/keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", "dev": true, - "requires": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.0.0" + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" } }, - "filesize": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.11.tgz", - "integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==", + "node_modules/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 }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "node_modules/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": { - "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" - } - } + "language-subtag-registry": "~0.3.2" } }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "node_modules/last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", "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" + "engines": { + "node": ">= 10.13.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==", + "node_modules/lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" } }, - "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==", + "node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", "dev": true, - "requires": { - "locate-path": "^3.0.0" + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" } }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" + "engines": { + "node": ">=6" } }, - "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==", + "node_modules/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": { - "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" + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "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==", + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "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 - } + "immediate": "~3.0.5" } }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "node_modules/liftoff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", "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" - } - } + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "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==", + "node_modules/liftoff/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "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" - } - } + "engines": { + "node": ">=0.10.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" - }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, "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=" - } + "uc.micro": "^1.0.1" } }, - "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" - } - } + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "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==", + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "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" - } - } + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.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==", + "node_modules/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": { - "@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" - } - } + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "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" - } + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "node_modules/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 }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "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" - } + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true }, - "free-style": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/free-style/-/free-style-3.1.0.tgz", - "integrity": "sha512-vJujYSIyT30iDoaoeigNAxX4yB1RUrh+N2ZMhIElMr3BvCuGXOw7XNJMEEJkDUeamK2Rnb/IKFGKRKlTWIGRWA==", + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "dev": true }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "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" - } - } - } + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true }, - "fromentries": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", - "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "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==", + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "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" + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "minipass": "^2.6.0" + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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=", + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "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==", + "node_modules/log-symbols/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/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 }, - "fs-walk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/fs-walk/-/fs-walk-0.0.1.tgz", - "integrity": "sha1-9/yRw64e6tB8mYvF0N1B8tvr0zU=", + "node_modules/log-symbols/node_modules/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, - "requires": { - "async": "*" + "engines": { + "node": ">=8" } }, - "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=", + "node_modules/log-symbols/node_modules/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": { - "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" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "node_modules/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==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } }, - "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "dependencies": { + "yallist": "^4.0.0" }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/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 + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "node_modules/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 + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/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, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/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 + }, + "node_modules/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 + }, + "node_modules/minimatch": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/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, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "optional": true + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha-junit-reporter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.0.2.tgz", + "integrity": "sha512-vYwWq5hh3v1lG0gdQCBxwNipBfvDiAM1PHroQRNp96+2l72e9wEUTw+mzoK+O0SudgfQ7WvTQZ9Nh3qkAYAjfg==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^6.0.1", + "xml": "^1.0.0" + }, + "peerDependencies": { + "mocha": ">=2.2.5" + } + }, + "node_modules/mocha-multi-reporters": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz", + "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "mocha": ">=3.1.2" + } + }, + "node_modules/mocha-multi-reporters/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { "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, + } + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { "optional": true + } + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/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, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, + "node_modules/mrmime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", + "integrity": "sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/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==" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "optional": true + }, + "node_modules/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 + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/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_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/node-abi": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", + "dev": true, + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "optional": true + }, + "node_modules/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, + "dependencies": { + "fs-walk": "0.0.1" + }, + "bin": { + "node-has-native-dependencies": "index.js" + } + }, + "node_modules/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, + "dependencies": { + "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" + } + }, + "node_modules/node-libs-browser/node_modules/buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "deprecated": "This version of 'buffer' is out-of-date. You must update to v4.9.2 or newer", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "node_modules/node-loader": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-1.0.3.tgz", + "integrity": "sha512-8c9ef5q24F0AjrPxUjdX7qdTlsU1zZCPeqYvSBCH1TJko3QW4qu1uA1C9KbOPdaRQwREDdbSYZgltBAlbV7l5g==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/node-polyfill-webpack-plugin": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-1.1.4.tgz", + "integrity": "sha512-Z0XTKj1wRWO8o/Vjobsw5iOJCN+Sua3EZEUc2Ziy9CyVvmHKu6o+t4gUH9GOE0czyPR94LI6ZCV/PpcM8b5yow==", + "dev": true, + "dependencies": { + "assert": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "console-browserify": "^1.2.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.12.0", + "domain-browser": "^4.19.0", + "events": "^3.3.0", + "filter-obj": "^2.0.2", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "punycode": "^2.1.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.3.0", + "timers-browserify": "^2.0.12", + "tty-browserify": "^0.0.1", + "url": "^0.11.0", + "util": "^0.12.4", + "vm-browserify": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "webpack": ">=5" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/assert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", + "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "dev": true, + "dependencies": { + "es6-object-assign": "^1.1.0", + "is-nan": "^1.2.1", + "object-is": "^1.0.1", + "util": "^0.12.0" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "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 + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "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" - } + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/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, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/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, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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, + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-run-path/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@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", + "get-package-type": "^0.1.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.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "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", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/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, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.hasown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", + "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/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, + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/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 + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "p-timeout": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/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, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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, + "engines": { + "node": ">=6" + } + }, + "node_modules/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, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-asn1/node_modules/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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "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" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/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 + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pbkdf2/node_modules/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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postinstall-build": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz", + "integrity": "sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==", + "deprecated": "postinstall-build's behavior is now built into npm! You should migrate off of postinstall-build and use the new `prepare` lifecycle script with npm 5.0.0 or greater.", + "dev": true, + "bin": { + "postinstall-build": "cli.js" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/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, + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz", + "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/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 + }, + "node_modules/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, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "dependencies": { + "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" + } + }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/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, + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/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, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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, + "dependencies": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "dependencies": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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 + }, + "node_modules/replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replace-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { "optional": true + } + } + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "dev": true, + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rewiremock": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-runtime": "^6.26.0", + "compare-module-exports": "^2.1.0", + "node-libs-browser": "^2.1.0", + "path-parse": "^1.0.5", + "wipe-node-cache": "^2.1.2", + "wipe-webpack-cache": "^2.1.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "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" - } + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs-compat": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz", + "integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw==" + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/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 + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-greatest-satisfied-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", + "dev": true, + "dependencies": { + "sver": "^1.8.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/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 + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sha.js/node_modules/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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "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" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "node_modules/shortid": { + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true, - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length/node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==" + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "engines": { + "node": "*" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/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, + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/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 + }, + "node_modules/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, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/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 + }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==" + }, + "node_modules/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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", + "dev": true, + "optionalDependencies": { + "semver": "^6.3.0" + } + }, + "node_modules/sver/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-fs/node_modules/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, + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/tar-fs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tas-client": { + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", + "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { "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, + "esbuild": { "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, + "uglify-js": { "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/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, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/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 + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/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, + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/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, + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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 + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer/node_modules/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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "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 + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "dev": true, - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "license": "MIT" + }, + "node_modules/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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "node_modules/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": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "dependencies": { + "through2": "^2.0.3" }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", "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" - } - } + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } }, - "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==", + "node_modules/ts-loader": { + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", + "integrity": "sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==", "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "is-callable": "^1.1.3" + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" } }, - "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=", + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/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 }, - "fuzzy": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", - "integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg=" + "node_modules/ts-loader/node_modules/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, + "engines": { + "node": ">=8" + } }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "node_modules/ts-loader/node_modules/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": { - "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": { + "has-flag": "^4.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, "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 + "lodash": "^4.17.5" + } + }, + "node_modules/ts-node": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": 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" - } + "@swc/wasm": { + "optional": true } } }, - "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==" + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } }, - "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 + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz", + "integrity": "sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^3.9.0" + } }, - "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 + "node_modules/tsconfig-paths-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "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==", + "node_modules/tsconfig-paths-webpack-plugin/node_modules/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": { - "npm-conf": "^1.1.0" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.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==", + "node_modules/tsconfig-paths-webpack-plugin/node_modules/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 }, - "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 + "node_modules/tsconfig-paths-webpack-plugin/node_modules/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, + "engines": { + "node": ">=8" + } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true + "node_modules/tsconfig-paths-webpack-plugin/node_modules/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "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" + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" } }, - "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" + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" } }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "node_modules/tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "node_modules/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 + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "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" - } - } + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "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" - }, + "optional": true, "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" - } - } + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" } }, - "glob-watcher": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", - "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", + "node_modules/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": { - "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" + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "glob2base": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", - "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "node_modules/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, - "requires": { - "find-index": "^0.1.1" + "engines": { + "node": ">=4" } }, - "global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "node_modules/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, - "requires": { - "min-document": "^2.19.0", - "process": "^0.11.10" + "engines": { + "node": ">=8" } }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" } }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "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": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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=", + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "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 - } + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, - "requires": { - "sparkles": "^1.0.0" + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "got": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", - "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "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 - } + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" } }, - "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==", + "node_modules/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": { - "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" - } - } + "is-typedarray": "^1.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==", + "node_modules/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": { - "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" - } - } + "hasInstallScript": true, + "dependencies": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + }, + "engines": { + "node": ">=6.0.0" } }, - "gulp-chmod": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz", - "integrity": "sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw=", + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, - "requires": { - "deep-assign": "^1.0.0", - "stat-mode": "^0.2.0", - "through2": "^2.0.0" + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, - "gulp-filter": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", - "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM=", + "node_modules/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 + }, + "node_modules/uint64be": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-3.0.0.tgz", + "integrity": "sha512-mliiCSrsE29aNBI7O9W5gGv6WmA9kBR8PtTt6Apaxns076IRdYrrtFhXHEWMj5CSum3U7cv7/pi4xmi4XsIOqg==" + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, - "requires": { - "multimatch": "^2.0.0", - "plugin-error": "^0.1.2", - "streamfilter": "^1.0.5" + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "gulp-gunzip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.1.0.tgz", - "integrity": "sha512-3INeprGyz5fUtAs75k6wVslGuRZIjKAoQp39xA7Bz350ReqkrfYaLYqjZ67XyIfLytRXdzeX04f+DnBduYhQWw==", + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, - "requires": { - "through2": "~2.0.3", - "vinyl": "~2.0.1" + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, + "node_modules/undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", + "dev": true, + "dependencies": { + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, "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" - } + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz", + "integrity": "sha512-BjinxTXkbm9Jomp/YBTMGusr4fxIG67fNGShHIRAL16Ur2GJTq2xvLi+sxuiJmInCmwqqev2BCFKyvbfp/yAkg==", + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/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, + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.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==", + "node_modules/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, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "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==", + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "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" + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0" }, + "engines": { + "node": ">=4" + } + }, + "node_modules/url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/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, "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 - } + "inherits": "2.0.3" } }, - "gulp-typescript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-4.0.2.tgz", - "integrity": "sha512-Hhbn5Aa2l3T+tnn0KqsG6RRJmcYEsr3byTL2nBpNBeAK8pqug9Od4AwddU4JEI+hRw7mzZyjRbB8DDWR6paGVA==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", + "dev": true + }, + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "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" + "engines": { + "node": ">= 10.13.0" } }, - "gulp-untar": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.8.tgz", - "integrity": "sha512-mqW7v2uvrxd8IoCCwJ04sPYgWjR3Gsi6yfhVWBK3sFMDP7FuoT7GNmxrCMwkk4RWqQohx8DRv+cDq4SRDXATGA==", + "node_modules/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, - "requires": { - "event-stream": "3.3.4", - "streamifier": "~0.1.1", - "tar": "^2.2.1", - "through2": "~2.0.3", - "vinyl": "^1.2.0" + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, "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 + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/vinyl-contents/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "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" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "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" - } + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.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==", + "node_modules/vinyl-contents/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "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": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vinyl-contents/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-contents/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/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, + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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, + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/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, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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 + }, + "node_modules/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==", + "deprecated": "This package has been renamed to @vscode/debugprotocol, please update to the new name" + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.0-next.5", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.5.tgz", + "integrity": "sha512-Sl/8RAJtfF/2x/TPBVRuhzRAcqYR/QDjEjNqMcoKFfqsxfVUPzikupRDQYB77Gkbt1RrW43sSuZ5uLtNAcikQQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "10.0.0-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.12.tgz", + "integrity": "sha512-q7cVYCcYiv+a+fJYCbjMMScOGBnX162IBeUMFg31mvnN7RHKx5/CwKaCz+r+RciJrRXMqS8y8qpEVGgeIPnbxg==", + "dependencies": { + "minimatch": "^9.0.3", + "semver": "^7.6.0", + "vscode-languageserver-protocol": "3.17.6-next.10" }, + "engines": { + "vscode": "^1.91.0" + } + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "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" - } - } + "balanced-match": "^1.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" + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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" + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.6-next.10", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.10.tgz", + "integrity": "sha512-KOrrWn4NVC5jnFC5N6y/fyNKtx8rVYr67lhL/Z0P4ZBAN27aBsCnLBWAMIkYyJ1K8EZaE5r7gqdxrS9JPB6LIg==", + "dependencies": { + "vscode-jsonrpc": "9.0.0-next.5", + "vscode-languageserver-types": "3.17.6-next.5" } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "node_modules/vscode-languageserver-types": { + "version": "3.17.6-next.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", + "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" }, - "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" + "node_modules/vscode-tas-client": { + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz", + "integrity": "sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w==", + "dependencies": { + "tas-client": "0.2.33" + }, + "engines": { + "vscode": "^1.85.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" + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } } }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "node_modules/webpack-bundle-analyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", + "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", "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 - } + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" } }, - "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 + "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "node_modules/webpack-bundle-analyzer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "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 + "node_modules/webpack-bundle-analyzer/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "node_modules/webpack-bundle-analyzer/node_modules/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-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==", + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, - "requires": { - "has-symbol-support-x": "^1.4.1" + "engines": { + "node": ">= 10" } }, - "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=", + "node_modules/webpack-bundle-analyzer/node_modules/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, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "engines": { + "node": ">=8" } }, - "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=", + "node_modules/webpack-bundle-analyzer/node_modules/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": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" + "dependencies": { + "has-flag": "^4.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-cli": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", + "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", + "dev": true, "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" - } + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.1.1", + "@webpack-cli/info": "^1.4.1", + "@webpack-cli/serve": "^1.6.1", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "@webpack-cli/migrate": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true } } }, - "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" + "node_modules/webpack-cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" } }, - "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" + "node_modules/webpack-cli/node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" } }, - "hasha": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", - "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", "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 - } + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" } }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/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 }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" } }, - "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==", + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", "dev": true, - "requires": { - "react-is": "^16.7.0" + "engines": { + "node": ">=6" } }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "node_modules/webpack-require-from": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/webpack-require-from/-/webpack-require-from-1.8.6.tgz", + "integrity": "sha512-QmRsOkOYPKeNXp4uVc7qxnPrFQPrP4bhOc/gl4QenTFNgXdEbF1U8VC+jM/Sljb0VzJLNgyNiHlVkuHjcmDtBQ==", "dev": true, - "requires": { - "parse-passwd": "^1.0.0" + "peerDependencies": { + "tapable": "^2.2.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 + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } }, - "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==", + "node_modules/webpack/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "requires": { - "array-filter": "^1.0.0" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "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==", + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" } }, - "html-escaper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", - "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "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==", + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "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": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "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 - } + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "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==", + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, - "requires": { - "domhandler": "^3.0", - "htmlparser2": "^4.1.0", - "lodash.camelcase": "^4.3.0", - "ramda": "^0.27" + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", "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" - } - } + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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=", + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "node_modules/winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=" + }, + "node_modules/wipe-node-cache": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.2.tgz", + "integrity": "sha512-m7NXa8qSxBGMtdQilOu53ctMaIBXy93FOP04EC1Uf4bpsE+r+adfLKwIMIvGbABsznaSNxK/ErD4xXDyY5og9w==", + "dev": true + }, + "node_modules/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": { - "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" - } - } + "wipe-node-cache": "^2.1.0" } }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", "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" + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, - "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==", + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "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 + "license": "Apache-2.0" }, - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "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 + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, - "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==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "agent-base": "4", - "debug": "3.1.0" + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/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, + "license": "MIT", "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 - } + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "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==", + "node_modules/wrap-ansi-cjs/node_modules/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, - "requires": { - "@types/node": "^10.0.3" + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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" + "node_modules/wrap-ansi/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.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=", + "node_modules/wrap-ansi/node_modules/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 }, - "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==", + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/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": { - "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" - } - } + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, - "husky": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz", - "integrity": "sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==", + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "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" + "engines": { + "node": ">=8.3.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 + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": 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" - } + "utf-8-validate": { + "optional": true } } }, - "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" + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" } }, - "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 + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } }, - "icss-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", - "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "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" - } - } + "engines": { + "node": ">=0.4" } }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true + "node_modules/yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": ">=8" + } }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true, - "requires": { - "minimatch": "^3.0.4" + "engines": { + "node": ">=10" } }, - "image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, - "optional": true + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } }, - "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==" + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "import-cwd": { + "node_modules/yargs-unparser/node_modules/is-plain-obj": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, - "requires": { - "import-from": "^2.1.0" + "engines": { + "node": ">=8" } }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "node_modules/yargs/node_modules/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": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "node_modules/yargs/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, - "requires": { - "resolve-from": "^3.0.0" + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.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==", + "node_modules/yargs/node_modules/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": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "node_modules/yargs/node_modules/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 }, - "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=", + "node_modules/yargs/node_modules/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 }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "node_modules/yargs/node_modules/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 }, - "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==", + "node_modules/yargs/node_modules/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, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/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, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, "requires": { - "once": "^1.3.0", - "wrappy": "1" + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" } }, - "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 + "@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "requires": { + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } }, - "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" + "@azure/core-auth": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.2.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 - }, - "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" - } + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, - "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==", + "@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", "dev": true, "requires": { - "es-abstract": "^1.17.0-next.1", - "has": "^1.0.3", - "side-channel": "^1.0.2" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.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==", + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "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" + "tslib": "^2.6.2" } }, - "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==", + "@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", "dev": true, "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.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==", + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true + } + } + }, + "@azure/core-rest-pipeline": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" }, - "is-callable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", - "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", - "dev": true + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } }, - "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", - "dev": true, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "requires": { - "has-symbols": "^1.0.1" + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" } }, - "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 + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, - "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, + "@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", "requires": { - "loose-envify": "^1.0.0" + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } } }, - "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, + "@azure/core-util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } } }, - "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" + "@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "dev": true, + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.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=", + "@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + } } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true } } }, - "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": { + "@azure/logger": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dev": true, + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } } }, - "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=", + "@azure/msal-browser": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.14.0.tgz", + "integrity": "sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==", "dev": true, "requires": { - "binary-extensions": "^1.0.0" + "@azure/msal-common": "14.10.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==", + "@azure/msal-common": { + "version": "14.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", + "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", "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=", + "@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", "dev": true, "requires": { - "kind-of": "^3.0.2" + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.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" - } + "@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", + "dev": true } } }, - "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, + "@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.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 + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, - "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=", + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "requires": { - "ip-regex": "^2.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" } }, - "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=", + "@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", "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" + "@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" }, "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "ms": "2.1.2" } } } }, - "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 + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } }, - "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==", + "@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "dev": true, "requires": { - "got": "^9.6.0", - "p-any": "^2.0.0", - "p-timeout": "^3.0.0", - "public-ip": "^3.0.0" + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" }, "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==", + "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": { - "p-finally": "^1.0.0" + "yallist": "^3.0.2" } }, - "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" - } + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, - "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=", + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "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==", + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "is-path-inside": "^1.0.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.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=", + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "path-is-inside": "^1.0.1" + "@babel/types": "^7.22.5" } }, - "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==", + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, "requires": { - "isobject": "^3.0.1" + "@babel/types": "^7.22.5" } }, - "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=", + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", "dev": true, "requires": { - "has": "^1.0.1" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" } }, - "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==", + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "requires": { - "is-unc-path": "^1.0.0" + "@babel/types": "^7.22.5" } }, - "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 + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } }, - "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==", + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "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=", + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "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=", + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" } }, - "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==", + "@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, "requires": { - "unc-path-regex": "^0.1.2" + "@babel/types": "^7.27.1" } }, - "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=", + "@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "dev": true }, - "is2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", - "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", + "@babel/runtime-corejs3": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", + "dev": true, "requires": { - "deep-is": "^0.1.3", - "ip-regex": "^2.1.0", - "is-url": "^1.2.2" + "core-js-pure": "^3.30.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=", + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.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==", + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" + "ms": "2.1.2" } } } }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } }, - "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==", + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", "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==", + "@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, "requires": { - "append-transform": "^2.0.0" + "@cspotcode/source-map-consumer": "0.8.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==", + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "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" + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "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" - } + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, - "@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==", + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" + "ms": "^2.1.3" } }, - "@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==", + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { - "@babel/types": "^7.7.4" + "type-fest": "^0.20.2" } }, - "@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==", + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { - "@babel/types": "^7.7.4" + "argparse": "^2.0.1" } }, - "@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" + "brace-expansion": "^1.1.7" } }, - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "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==", + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true + }, + "@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", + "dev": true + }, + "@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "requires": { + "is-negated-glob": "^1.0.0" + } + }, + "@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "safe-buffer": "~5.1.1" + "ms": "^2.1.3" } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { - "ms": "^2.1.1" + "brace-expansion": "^1.1.7" } }, - "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=", + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "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==", + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==" + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "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" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^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==", + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.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==", + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "requires": { - "semver": "^6.0.0" + "ansi-regex": "^6.0.1" } }, - "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==", + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "requires": { - "aggregate-error": "^3.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.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 - }, - "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", - "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" + } + }, + "@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@microsoft/1ds-core-js": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", + "requires": { + "@microsoft/applicationinsights-core-js": "2.8.15", + "@microsoft/applicationinsights-shims": "^2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + } + }, + "@microsoft/1ds-post-js": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", + "requires": { + "@microsoft/1ds-core-js": "3.2.13", + "@microsoft/applicationinsights-shims": "^2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + } + }, + "@microsoft/applicationinsights-channel-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", + "requires": { + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "dependencies": { + "@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "requires": { - "glob": "^7.1.3" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" } }, - "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, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", "requires": { - "shebang-regex": "^3.0.0" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "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": { + "@microsoft/dynamicproto-js": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "requires": { - "isexe": "^2.0.0" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, - "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, + "@microsoft/applicationinsights-common": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "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, + "@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "requires": { - "semver": "^6.0.0" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } }, - "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, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "requires": { - "has-flag": "^4.0.0" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, - "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, + "@microsoft/applicationinsights-core-js": { + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.9" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz", + "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" + }, + "@microsoft/applicationinsights-web-basic": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", + "requires": { + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, + "@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "requires": { - "ms": "^2.1.1" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, - "istanbul-reports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", - "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", + "@microsoft/applicationinsights-web-snippet": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", + "integrity": "sha512-2IHAOaLauc8qaAitvWS+U931T+ze+7MNWrDHY47IENP5y2UA0vqJDu67kWZDdpCN1fFC77sfgfB+HV7SrKshnQ==" + }, + "@microsoft/dynamicproto-js": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", + "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" + }, + "@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "requires": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" } }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@opentelemetry/api": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==" + }, + "@opentelemetry/core": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", + "requires": { + "@opentelemetry/semantic-conventions": "1.15.2" + } + }, + "@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "requires": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" + } + }, + "@opentelemetry/resources": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", + "requires": { + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", + "requires": { + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==" + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "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": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" + "type-detect": "4.0.8" } }, - "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==", + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, "requires": { - "es-get-iterator": "^1.0.2", - "iterate-iterator": "^1.0.1" + "@sinonjs/commons": "^3.0.0" } }, - "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": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", - "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^6.1.0" + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" }, "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==", + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "type-detect": "4.0.8" } } } }, - "jpeg-js": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.7.tgz", - "integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==", + "@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true }, - "jquery": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", - "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==", + "@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 }, - "jquery-ui": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz", - "integrity": "sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=", + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", "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==", + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", "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==" + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "@types/bent": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@types/bent/-/bent-7.3.3.tgz", + "integrity": "sha512-5NEIhVzHiZ6wMjFBmJ3gwjxwGug6amMoAn93rtDBttwrODxm+bt63u+MJA7H9NGGM4X1m73sJrAxDapktl036Q==", "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" - } - } + "@types/node": "*" } }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", "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=" + "@types/chai-arrays": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/chai-arrays/-/chai-arrays-2.0.0.tgz", + "integrity": "sha512-5h5jnAC9C64YnD7WJpA5gBG7CppF/QmoWytOssJ6ysENllW49NBdpsTx6uuIBOpnzAnXThb8jBICgB62wezTLQ==", + "dev": true, + "requires": { + "@types/chai": "*" + } }, - "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=", + "@types/chai-as-promised": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz", + "integrity": "sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==", + "dev": true, "requires": { - "jsonparse": "~1.2.0" + "@types/chai": "*" } }, - "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==", + "@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 }, - "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 + "@types/decompress": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", + "integrity": "sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ==", + "dev": true, + "requires": { + "@types/node": "*" + } }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "@types/download": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.3.tgz", + "integrity": "sha512-IDwXjU7zCtuFVvI0Plnb02TpXyj3RA4YeOKQvEfsjdJeWxZ9hTl6lxeNsU2bLWn0aeAS7fyMl74w/TbdOlS2KQ==", + "dev": true, + "requires": { + "@types/decompress": "*", + "@types/got": "^9", + "@types/node": "*" + } }, - "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==" + "@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } }, - "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=", + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "requires": { - "jsonify": "~0.0.0" + "@types/eslint": "*", + "@types/estree": "*" } }, - "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=", + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "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 + "@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "requires": { + "@types/jsonfile": "*", + "@types/node": "*" + } }, - "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=" + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } }, - "json2csv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-4.5.2.tgz", - "integrity": "sha512-Te2Knce3VfLKyurH3AolM6Y781ZE+R3jQ+0YQ0HYLiubyicST/19vML24e1dpScaaEQb+1Q1t5IcGXr6esM9Lw==", + "@types/got": { + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz", + "integrity": "sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA==", "dev": true, "requires": { - "commander": "^2.15.1", - "jsonparse": "^1.3.1", - "lodash.get": "^4.4.2" + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" }, "dependencies": { - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.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 } } }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "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==", + "@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/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, "requires": { - "minimist": "^1.2.0" + "@types/node": "*" } }, - "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==" + "@types/lodash": { + "version": "4.14.181", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", + "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", + "dev": true }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/mocha": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", + "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", + "dev": true + }, + "@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, "requires": { - "graceful-fs": "^4.1.6" + "undici-types": "~6.21.0" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "@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 }, - "jsonparse": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", - "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" + "@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" }, - "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" - } + "@types/shortid": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", + "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=", + "dev": true }, - "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==", + "@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", "dev": true, "requires": { - "array-includes": "^3.1.1", - "object.assign": "^4.1.0" + "@types/sinonjs__fake-timers": "*" } }, - "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=", + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", "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==", + "@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 }, - "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" - } + "@types/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "dev": true }, - "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==", + "@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", "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" - } + "@types/vscode": { + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", + "dev": true }, - "labella": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/labella/-/labella-1.1.4.tgz", - "integrity": "sha1-xsxaNA6N80DrM1YzaD6lm4KMMi0=", + "@types/which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz", + "integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==", "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==", + "@types/winreg": { + "version": "1.2.31", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.31.tgz", + "integrity": "sha512-SDatEMEtQ1cJK3esIdH6colduWBP+42Xw9Guq1sf/N6rM3ZxgljBduvZOwBsxRps/k5+Wwf5HJun6pH8OnD2gg==", "dev": true }, - "language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", "dev": true, "requires": { - "language-subtag-registry": "~0.3.2" + "@types/node": "*" } }, - "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" + "@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "requires": { - "readable-stream": "^2.0.5" + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" }, "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==", + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "ms": "2.1.2" } } } }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "requires": { - "invert-kv": "^1.0.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" } }, - "lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "requires": { - "flush-write-stream": "^1.0.2" + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, - "leaflet": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.5.1.tgz", - "integrity": "sha512-ekM9KAeG99tYisNBg0IzEywAlp0hYI5XRipsqRXyRTeuU8jcuntilpp+eFf5gaE0xubc9RuSNIVtByEKwqFV0w==", + "@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true }, - "less": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz", - "integrity": "sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w==", + "@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "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" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.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 + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } } } }, - "less-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-5.0.0.tgz", - "integrity": "sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg==", + "@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "requires": { - "clone": "^2.1.1", - "loader-utils": "^1.1.0", - "pify": "^4.0.1" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" }, "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "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==", + "@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "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" + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" } }, - "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=", + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, - "linebreak": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.0.2.tgz", - "integrity": "sha512-bJwSRsJeAmaZYnkcwl5sCQNfSDAhBuXxb6L27tb+qkBRtUQSSTUa5bcgCPD6hFEkRNlpWHfK7nFMmcANU7ZP1w==", + "@vscode/extension-telemetry": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", "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" - } - } + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" } }, - "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 + "@vscode/test-electron": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.8.tgz", + "integrity": "sha512-b4aZZsBKtMGdDljAsOPObnAi7+VWIaYl3ylCz1jTs+oV6BZ4TNHcVNC3xUn0azPeszBmwSBDQYfFESIaUQnrOg==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + } }, - "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=", + "@vscode/vsce": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.27.0.tgz", + "integrity": "sha512-FFUMBVSyyjjJpWszwqk7d4U3YllY8FdWslbUDMRki1x4ZjA3Z0hmRMfypWrjP9sptbSR9nyPFU4uqjhy2qRB/w==", "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" + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "keytar": "^7.7.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" }, "dependencies": { - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "requires": { - "error-ex": "^1.2.0" + "lru-cache": "^6.0.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==", + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { - "minimist": "^1.2.0" + "brace-expansion": "^1.1.7" } } } }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "@vscode/vsce-sign": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.4.tgz", + "integrity": "sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" } }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "dev": true, + "optional": true }, - "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 + "@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "dev": true, + "optional": 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 + "@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "dev": true, + "optional": 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 + "@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "dev": true, + "optional": 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 + "@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "dev": true, + "optional": 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=" + "@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "dev": true, + "optional": true }, - "lodash.curry": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", - "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=", - "dev": true + "@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "dev": true, + "optional": 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 + "@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "dev": true, + "optional": 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 + "@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "dev": true, + "optional": 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 + "@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } }, - "lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=", + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "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=", + "@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "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=", + "@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "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 + "@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "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 + "@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } }, - "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 + "@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", - "dev": true + "@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "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 + "@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", - "dev": true + "@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true + "@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } }, - "lodash.tail": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", - "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", - "dev": true + "@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } }, - "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==", + "@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", + "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", + "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", "dev": true, "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" + "envinfo": "^7.7.3" } }, - "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==", + "@webpack-cli/serve": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", + "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } + "requires": {} }, - "lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "@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 }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "@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 }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" }, - "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==", + "acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "requires": {} + }, + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "requires": { - "chalk": "^2.4.2" - } + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true }, - "log4js": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.1.2.tgz", - "integrity": "sha512-knS4Y30pC1e0n7rfx3VxcLOdBCsEo0o6/C7PVTGxdVK+5b1TYOSGQPn9FDcrhkoQBV29qwmA2mtkznPAQKnxQg==", + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "requires": { - "date-format": "^3.0.0", - "debug": "^4.1.1", - "flatted": "^2.0.1", - "rfdc": "^1.1.4", - "streamroller": "^2.2.3" + "debug": "4" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } } } }, - "logform": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", - "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "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==", + "dev": true, "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" } }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "requires": { - "@sinonjs/commons": "^1.7.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "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==", + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } } }, - "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==" + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} }, - "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==", + "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": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "ansi-wrap": "^0.1.0" } }, - "lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "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": { - "es5-ext": "~0.10.2" + "color-convert": "^1.9.0" } }, - "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==", + "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": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "requires": { - "vlq": "^0.2.2" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" } }, - "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==", + "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": { - "pify": "^4.0.1", - "semver": "^5.6.0" + "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 + } } }, - "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==", + "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": { - "kind-of": "^6.0.2" + "default-require-extensions": "^3.0.0" } }, - "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 + "applicationinsights": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", + "requires": { + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", + "@microsoft/applicationinsights-web-snippet": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" + } }, - "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==", + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==" + }, + "archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", "dev": true, "requires": { - "p-defer": "^1.0.0" + "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": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", + "dev": true + } } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", "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=", + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { - "object-visit": "^1.0.0" + "sprintf-js": "~1.0.2" } }, - "markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", + "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 }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", + "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 }, - "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=", + "array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "requires": { - "bintrees": "^1.0.1", - "tinyqueue": "^1.1.0" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" } }, - "matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true + }, + "array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "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" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" } }, - "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==", + "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 }, - "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==", + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "md5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", - "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, "requires": { - "charenc": "~0.0.1", - "crypt": "~0.0.1", - "is-buffer": "~1.1.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" } }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" } }, - "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==", + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "requires": { - "unist-util-visit-parents": "1.1.2" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" } }, - "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==", + "arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "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 - } + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" } }, - "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==", + "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": { - "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" + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" } }, - "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=", + "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": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" + "object-assign": "^4.1.1", + "util": "0.10.3" }, "dependencies": { - "process-nextick-args": { + "inherits": { "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==", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", "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==", + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "inherits": "2.0.1" } } } }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "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 }, - "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==", + "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 }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "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 }, - "microevent.ts": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", - "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "dev": true }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "async-done": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "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" + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.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, + "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": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" + "stack-chain": "^1.3.7" } }, - "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==", + "async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", "requires": { - "mime-db": "1.40.0" + "semver": "^5.3.0", + "shimmer": "^1.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + } } }, - "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 + "async-settle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", + "dev": true, + "requires": { + "async-done": "^2.0.0" + } }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "requires": { - "dom-walk": "^0.1.0" + "possible-typed-array-names": "^1.0.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==" + "axe-core": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz", + "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", + "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=", + "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 }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "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=", "dev": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" }, "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "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.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "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==", + "bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, "requires": { - "minipass": "^3.0.0" + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" }, "dependencies": { - "minipass": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", - "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "requires": { - "yallist": "^4.0.0" + "once": "^1.4.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==", + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "dev": true, + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true + }, + "bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", "dev": true, "requires": { - "minipass": "^3.0.0" + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" }, "dependencies": { - "minipass": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", - "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", - "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==", + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true } } }, - "minipass-pipeline": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", - "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", + "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.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", - "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", - "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 - } + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "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 + }, + "brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "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" } }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "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": { - "minipass": "^2.9.0" + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" } }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "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": { - "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" - } - } + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "dev": true, "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.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" - } + "bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true + }, + "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 } } }, - "mixin-object": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", - "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "dev": true, "requires": { - "for-in": "^0.1.3", - "is-extendable": "^0.1.1" + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" }, "dependencies": { - "for-in": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", - "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true + }, + "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 } } }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "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": { - "minimist": "^1.2.5" + "pako": "~1.0.5" } }, - "mocha": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.0.1.tgz", - "integrity": "sha512-vefaXfdYI8+Yo8nPZQQi0QO2o+5q9UIMX1jZ1XMmK3+4+CQjc7+B0hPdUeglXiTlr8IHMVRo63IhO9Mzt6fxOg==", - "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": "3.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.0" - }, - "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.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", - "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": "3.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", - "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", - "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" - } - }, - "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" - } - } + "browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "requires": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^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==", + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "requires": { - "debug": "^2.2.0", - "md5": "^2.1.0", - "mkdirp": "~0.5.1", - "strip-ansi": "^4.0.0", - "xml": "^1.0.0" + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "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==", + "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 + }, + "bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==", + "dev": true + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", + "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": { - "ansi-regex": { + "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "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" - } + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", + "dev": true } } }, - "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=", + "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": { - "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" - } - } + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + "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 }, - "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==", + "caniuse-lite": { + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "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==", + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chai-arrays": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chai-arrays/-/chai-arrays-2.2.0.tgz", + "integrity": "sha512-4awrdGI2EH8owJ9I58PXwG4N56/FiM8bsn4CVSNEgr4GKAM6Kq5JPVApUbhUBjDakbZNuRvV7quRSC38PWq/tg==", "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==", + "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": { - "@types/webpack": "^4.4.19" + "check-error": "^1.0.2" } }, - "monaco-textmate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/monaco-textmate/-/monaco-textmate-3.0.1.tgz", - "integrity": "sha512-ZxxY3OsqUczYP1sGqo97tu+CJmMBwuSW+dL0WEBdDhOZ5G1zntw72hvBc68ZQAirosWvbDKgN1dL5k173QtFww==", + "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": { - "fast-plist": "^0.1.2" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, - "moo": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", - "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", "dev": true }, - "mount-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mount-point/-/mount-point-3.0.0.tgz", - "integrity": "sha1-Zly57evoDREOZY21bDHQrvUaj5c=", + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", "dev": true, "requires": { - "@sindresorhus/df": "^1.0.1", - "pify": "^2.3.0", - "pinkie-promise": "^2.0.1" + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" }, "dependencies": { - "@sindresorhus/df": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/df/-/df-1.0.1.tgz", - "integrity": "sha1-xptm9S9vzdKHyAffIQMF2694UA0=", + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", "dev": true }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "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==", + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "dev": true, "requires": { - "glob": "^7.1.3" + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true } } }, - "move-file": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/move-file/-/move-file-1.2.0.tgz", - "integrity": "sha512-USHrRmxzGowUWAGBbJPdFjHzEqtxDU03pLHY0Rfqgtnq+q8FOIs8wvkkf+Udmg77SJKs47y9sI0jJvQeYsmiCA==", + "cheerio-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", "dev": true, "requires": { - "cp-file": "^6.1.0", - "make-dir": "^3.0.0", - "path-exists": "^3.0.0" + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.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==", + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", "dev": true, "requires": { - "semver": "^6.0.0" + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", "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=", + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", "dev": true, "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "domelementtype": "^2.2.0" } }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", "dev": true, "requires": { - "glob": "^6.0.1" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true } } }, - "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==", + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "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" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.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 + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "optional": true }, - "nearley": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.16.0.tgz", - "integrity": "sha512-Tr9XD3Vt/EujXbZBv6UAHYoLUSMQAxSsTnm9K3koXzjzNWY195NqALeyrzLZBKzAkL3gl92BcSogqrHjD8QuUg==", + "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": { - "commander": "^2.19.0", - "moo": "^0.4.3", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" + "tslib": "^1.9.0" } }, - "needle": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", - "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" }, "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==", + "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 } } }, - "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==", + "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 }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, - "nise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-3.0.0.tgz", - "integrity": "sha512-EObFx5tioBMePHpU/gGczaY2YDqL255iwjmZwswu2CiwEW8xIGrr3E2xij+efIppS1nLQo9NyXSIUySGHUOhHQ==", + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "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" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.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" - } + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true }, - "nocache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", - "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==", + "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 }, - "nock": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", - "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "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" - } - } + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.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==" - }, - "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": { + "clone-response": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/node-has-native-dependencies/-/node-has-native-dependencies-1.0.2.tgz", - "integrity": "sha1-MVLsl1O2ZB5NMi0YXdSTBkmto9o=", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dev": true, "requires": { - "fs-walk": "0.0.1" + "mimic-response": "^1.0.0" } }, - "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==", + "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": { - "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 - } + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" } }, - "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, + "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": { - "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" + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.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" - } - } - } + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, - "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=", + "cockatiel": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.3.tgz", + "integrity": "sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==", "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==", + "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": { - "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" - } - } + "color-name": "1.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" - } + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, - "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, + "colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, + "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": { - "semver": "^5.3.0" + "delayed-stream": "~1.0.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==" + "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 }, - "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" - } + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true }, - "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" - } + "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 }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "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 }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "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 }, - "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==", + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "requires": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" + "safe-buffer": "5.2.1" }, "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" - } + "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 } } }, - "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, + "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": { - "once": "^1.3.2" + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" } }, - "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==", + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "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==", + "copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", "dev": true, "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" }, "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "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==", + "copy-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==", "dev": true, "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } } }, - "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=", + "core-js-pure": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", + "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.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, "requires": { - "path-key": "^2.0.0" + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "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": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.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==", + "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": { - "boolbase": "~1.0.0" + "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" } }, - "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==", + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, - "nyc": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.0.tgz", - "integrity": "sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==", + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "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" + "cross-spawn": "^7.0.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 - }, - "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==", + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { - "aggregate-error": "^3.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "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==", + "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 }, - "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": { + "shebang-command": { "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==", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "shebang-regex": "^3.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=", + "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 - }, - "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=", + "cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" }, "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" - } + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "isexe": "^2.0.0" } } } }, - "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==" + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true }, - "object-is": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + } + }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "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==" + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } }, - "object-visit": { + "data-view-byte-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", "dev": true, "requires": { - "isobject": "^3.0.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" } }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" } }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" + "ms": "2.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" - } + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, - "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==", + "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.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "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": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" + "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": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } } }, - "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==", + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.11.0", - "function-bind": "^1.1.1", - "has": "^1.0.1" + "mimic-response": "^1.0.0" } }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "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": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" + "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": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true + } } }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "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": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "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": { - "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" - } + "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 } } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "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": { - "isobject": "^3.0.1" + "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": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true + } } }, - "object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", "dev": true, "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" }, "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "dev": true, "requires": { - "for-in": "^1.0.1" + "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": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true } } }, - "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "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": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" + "type-detect": "^4.0.0" } }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "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, - "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=" + "optional": true }, - "onecolor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/onecolor/-/onecolor-3.1.0.tgz", - "integrity": "sha512-YZSypViXzu3ul5LMu/m6XjJ9ol8qAy9S2VjHl5E6UlhUH1KGKWabyEJifn0Jjpw23bYDzC2ucKMPGiH5kfwSGQ==", + "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 }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "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": { - "mimic-fn": "^1.0.0" + "strip-bom": "^4.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==", + "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 } } }, - "onigasm": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.2.tgz", - "integrity": "sha512-TQTMk+RmPYx4sGzNAgV0q7At7PABDNHVqZBlC4aRXHg8hpCdemLOF0qq0gUCjwUbc7mhJMBOo3XpTRYwyr45Gw==", + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "requires": { - "lru-cache": "^4.1.1" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, - "opener": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true }, - "opn": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.2.0.tgz", - "integrity": "sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==", + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "requires": { - "is-wsl": "^1.1.0" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, "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" + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.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=", + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "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": { - "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" - } - } + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.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==", + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true + }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, + "optional": true + }, + "diagnostic-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "requires": { - "url-parse": "^1.4.3" + "semver": "^7.5.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 + "diagnostic-channel-publishers": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", + "requires": {} }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "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": { - "lcid": "^1.0.0" + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.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==", + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "path-type": "^4.0.0" } }, - "p-any": { + "doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-any/-/p-any-2.1.0.tgz", - "integrity": "sha512-JAERcaMBLYKMq+voYw36+x5Dgh47+/o7yuv2oQYuSSUml4YeqJEFznBrY2UeEkoSHqBua6hz518n/PsowTYLLg==", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "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 + }, + "download": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/download/-/download-8.0.0.tgz", + "integrity": "sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==", + "dev": true, "requires": { - "p-cancelable": "^2.0.0", - "p-some": "^4.0.0", - "type-fest": "^0.3.0" + "archive-type": "^4.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.2.1", + "ext-name": "^5.0.0", + "file-type": "^11.1.0", + "filenamify": "^3.0.0", + "get-stream": "^4.1.0", + "got": "^8.3.1", + "make-dir": "^2.1.0", + "p-event": "^2.1.0", + "pify": "^4.0.1" }, "dependencies": { - "p-cancelable": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", - "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + "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" + } }, - "type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==" + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true } } }, - "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, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "requires": { - "p-timeout": "^2.0.1" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" } }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true }, - "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=", + "duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", "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==", + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dev": true, "requires": { - "p-try": "^2.0.0" + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } }, - "p-locate": { + "each-props": { "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==", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "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" + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" }, "dependencies": { - "p-cancelable": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", - "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true } } }, - "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==", + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "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==", + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" + "safe-buffer": "^5.0.1" } }, - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + "electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true }, - "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "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" - } - } + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" } }, - "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, + "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": { - "no-case": "^2.2.0" + "shimmer": "^1.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==", + "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 + }, + "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": { - "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 - } + "once": "^1.4.0" } }, - "parse-asn1": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", - "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "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" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" } }, - "parse-cache-control": { + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true + }, + "es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + } + }, + "es-define-property": { "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=", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "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, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "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" + "es-errors": "^1.3.0" } }, - "parse-filepath": { + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "es-shim-unscopables": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" + "hasown": "^2.0.0" } }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "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": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, - "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==", + "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 }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", "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" + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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 + }, + "eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.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" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "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" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "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 + }, + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "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 + }, + "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-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } } }, - "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=", + "eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", "dev": true, - "requires": { - "better-assert": "~1.0.0" - } + "requires": {} }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "requires": { - "better-assert": "~1.0.0" + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "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=", + "eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "requires": { - "path-root-regex": "^0.1.0" + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "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==", + "eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "requires": { - "isarray": "0.0.1" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" }, "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "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==", + "eslint-plugin-jsx-a11y": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", + "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", "dev": true, "requires": { - "pify": "^3.0.0" + "@babel/runtime": "^7.16.3", + "aria-query": "^4.2.2", + "array-includes": "^3.1.4", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.3.5", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.7", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.2.1", + "language-tags": "^1.0.5", + "minimatch": "^3.0.4" }, "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "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.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "eslint-plugin-no-only-tests": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", + "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==", "dev": true }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "eslint-plugin-react": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", + "integrity": "sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==", "dev": true, "requires": { - "through": "~2.3" + "array-includes": "^3.1.4", + "array.prototype.flatmap": "^1.2.5", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.0", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.6" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "eslint-plugin-react-hooks": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz", + "integrity": "sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ==", "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" - } + "requires": {} }, - "pdfkit": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.11.0.tgz", - "integrity": "sha512-1s9gaumXkYxcVF1iRtSmLiISF2r4nHtsTgpwXiK8Swe+xwk/1pm8FJjYqN7L3x13NsWnGyUFntWcO8vfqq+wwA==", + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "requires": { - "crypto-js": "^3.1.9-1", - "fontkit": "^1.8.0", - "linebreak": "^1.0.2", - "png-js": "^1.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" } }, - "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=", + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "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=", + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "pinkie": "^2.0.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" } }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "requires": { - "node-modules-regexp": "^1.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } } }, - "pixrem": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pixrem/-/pixrem-4.0.1.tgz", - "integrity": "sha1-LaSh3m7EQjxfw3lOkwuB1EkOxoY=", + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "browserslist": "^2.0.0", - "postcss": "^6.0.0", - "reduce-css-calc": "^1.2.7" + "estraverse": "^5.2.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" - } - }, - "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" - } + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "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" - } + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true }, - "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==", + "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.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "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": { - "playwright-core": "=0.13.0" + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" } }, - "playwright-core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-0.13.0.tgz", - "integrity": "sha512-bH9cOQhmbdXJnHX22PRZ79IdXv5wmLc9tHob2PmkT5qFilopT3INAIJcKkaLN1E/GqIDp0xGdB88Vr5f86g8pQ==", + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "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" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.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==", + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, "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" - } - } - } + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "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 }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "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": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "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 } } }, - "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==", + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, "requires": { - "semver-compare": "^1.0.0" + "homedir-polyfill": "^1.0.1" } }, - "pleeease-filters": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pleeease-filters/-/pleeease-filters-4.0.0.tgz", - "integrity": "sha1-ZjKy+wVkjSdY2GU4T7zteeHMrsc=", + "expose-loader": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", + "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", + "dev": true, + "requires": {} + }, + "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==", + "dev": true + }, + "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": { - "onecolor": "^3.0.4", - "postcss": "^6.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.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==", + "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": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "is-plain-object": "^2.0.4" } } } }, - "plotly.js-dist": { - "version": "1.54.5", - "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-1.54.5.tgz", - "integrity": "sha512-gIvJw5pG5ZeqhzJHcSS7Cl6Zfwl1etnMiVI951bJVyvh9q5Bjwiyo1pgqfcYcI14jCAlGXLi2G2s7At3YtPkKA==", + "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-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "dev": true }, - "plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", "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" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "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=", + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { - "kind-of": "^1.1.0" + "is-glob": "^4.0.1" } - }, - "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==", + "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=", "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==" + "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 + }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true }, - "pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", "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=", + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "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" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "martinez-polygon-clipping": "^0.1.5" + "flat-cache": "^3.0.4" } }, - "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==", + "file-type": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", + "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==", + "dev": true + }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true }, - "portfinder": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz", - "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==", + "filenamify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-3.0.0.tgz", + "integrity": "sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g==", + "dev": true, "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" - } - } + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.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=", + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "filter-obj": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz", + "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", "dev": true }, - "postcss": { - "version": "7.0.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", - "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "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" - } - } + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" } }, - "postcss-apply": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/postcss-apply/-/postcss-apply-0.8.0.tgz", - "integrity": "sha1-FOVEu7XLbxweBIhXll15rgZrE0M=", + "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" + } + }, + "findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, "requires": { - "babel-runtime": "^6.23.0", - "balanced-match": "^0.4.2", - "postcss": "^6.0.0" + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" }, "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "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": { + "flagged-respawn": { "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=", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", + "dev": true + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "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" - } - } + "flatted": "^3.1.0", + "rimraf": "^3.0.2" } }, - "postcss-calc": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-6.0.2.tgz", - "integrity": "sha512-fiznXjEN5T42Qm7qqMCVJXS3roaj9r4xsSi+meaBVe7CJBl8t/QLOXu02Z2E6oWAMWIvCuF6JrvzFekmVEbOKA==", + "flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "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" - } - } + "is-callable": "^1.2.7" } }, - "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==", + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", "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" - } - } + "for-in": "^1.0.1" } }, - "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==", + "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": { - "color": "^2.0.1", - "postcss": "^6.0.14", - "postcss-message-helpers": "^2.0.0", - "reduce-function-call": "^1.0.2" + "cross-spawn": "^7.0.0", + "signal-exit": "^3.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==", + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "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": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "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 } } }, - "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=", + "form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, "requires": { - "color": "^1.0.3", - "postcss": "^6.0.1", - "postcss-message-helpers": "^2.0.0" + "inherits": "^2.0.1", + "readable-stream": "^2.0.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": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^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, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "requires": { - "color-convert": "^1.8.2", - "color-string": "^1.4.0" + "graceful-fs": "^4.1.6", + "universalify": "^2.0.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" - } + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" } } }, - "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=", + "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": { - "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" - } - } + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" } }, - "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=", + "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": { - "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" - } - } + "async": "*" } }, - "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==", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "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" - } - } + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, - "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, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "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-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "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" - } - } + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.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=", + "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": { - "postcss": "^6.0.6", - "postcss-value-parser": "^3.3.0", - "rgb-hex": "^2.1.0" + "pump": "^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==", + "pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } } } }, - "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" - } - } + "get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" } }, - "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=", + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, + "optional": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { - "postcss": "^6.0.1" + "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" }, "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, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "brace-expansion": "^1.1.7" } } } }, - "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==", + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "requires": { - "balanced-match": "^1.0.0", - "postcss": "^6.0.18" + "is-glob": "^3.1.0", + "path-dirname": "^1.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==", + "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": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "is-extglob": "^2.1.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=", + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", "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" - } - } + "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" } }, - "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==", + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "glob-watcher": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "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" - } - } + "async-done": "^2.0.0", + "chokidar": "^3.5.3" } }, - "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=", + "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": { - "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" - } - } + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.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=", + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, "requires": { - "postcss": "^6.0.1", - "postcss-media-query-parser": "^0.2.3" + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" }, "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "isexe": "^2.0.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" - } + "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 }, - "postcss-initial": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-2.0.0.tgz", - "integrity": "sha1-cnFfczbgu3k1HZnuZcSiU6hEG6Q=", + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "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" - } - } + "define-properties": "^1.2.1", + "gopd": "^1.0.1" } }, - "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==", + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.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==", + "glogg": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", "dev": true, "requires": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" + "sparkles": "^2.1.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=", + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "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": { - "postcss": "^6.0.1" + "@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": { - "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" - } + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true } } }, - "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 + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "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=", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "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==", + "gulp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", "dev": true, "requires": { - "postcss": "^6.0.1" + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.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==", + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "graceful-fs": "^4.2.8", + "streamx": "^2.12.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==", + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "is-glob": "^4.0.3" } - } - } - }, - "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==", + }, + "glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" } - } - } - }, - "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==", + }, + "lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true + }, + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "once": "^1.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==", + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, + "resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "value-or-function": "^4.0.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==", + }, + "to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "streamx": "^2.12.5" } - } - } - }, - "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==", + }, + "value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" } - } - } - }, - "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==", + }, + "vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + } + }, + "vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.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=", + "gulp-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", "dev": true, "requires": { - "balanced-match": "^0.4.2", - "postcss": "^6.0.1" + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.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=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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 + }, + "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.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" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } } } }, - "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=", + "gulp-typescript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", + "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", "dev": true, "requires": { - "balanced-match": "^0.4.2", - "postcss": "^6.0.1" + "ansi-colors": "^3.0.5", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.3" }, "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "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 + }, + "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 }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "dev": true, "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "inherits": "^2.0.4", + "readable-stream": "2 || 3" } } } }, - "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==", + "gulplog": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", "dev": true, "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" + "glogg": "^2.2.0" } }, - "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==", + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, "requires": { - "fast-diff": "^1.1.2" + "duplexer": "^0.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=", + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "renderkid": "^2.0.1", - "utila": "~0.4" + "function-bind": "^1.1.1" } }, - "pretty-format": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", - "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "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-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "requires": { - "@jest/types": "^24.9.0", - "ansi-regex": "^4.0.0", - "ansi-styles": "^3.2.0", - "react-is": "^16.8.4" + "es-define-property": "^1.0.0" } }, - "pretty-hrtime": { + "has-proto": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "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=", + "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 }, - "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=" + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, - "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==", + "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": { - "fromentries": "^1.2.0" + "has-symbol-support-x": "^1.4.1" } }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "dev": true, "requires": { - "asap": "~2.0.3" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "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" + } }, - "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==", + "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": { - "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" + "is-stream": "^2.0.0", + "type-fest": "^0.8.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.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==", + "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 } } }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "function-bind": "^1.1.2" } }, - "prop-types-exact": { + "he": { "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==", + "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": { - "has": "^1.0.3", - "object.assign": "^4.1.0", - "reflect.ownkeys": "^0.2.0" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, - "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==", + "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": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" + "parse-passwd": "^1.0.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==", + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "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 }, - "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==", + "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": { - "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" + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" }, "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==", + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "ms": "2.1.2" } } } }, - "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" - } + "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 }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + } } }, - "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=", + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "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, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "requires": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true }, - "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true }, - "queue": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/queue/-/queue-3.1.0.tgz", - "integrity": "sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU=", + "import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "requires": { - "inherits": "~2.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "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 + } } }, - "quote-stream": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", - "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=", + "import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", "requires": { - "buffer-equal": "0.0.1", - "minimist": "^1.1.3", - "through2": "^2.0.0" + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" } }, - "raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "requires": { - "performance-now": "^2.1.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" } }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, - "ramda": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", - "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==", + "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==", "dev": true }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" + "once": "^1.3.0", + "wrappy": "1" } }, - "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" - } + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" } }, - "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==", + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "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==", + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", "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 - } + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" } }, - "raw-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", - "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", - "dev": true + "inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "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": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" } }, - "react": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", - "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.6" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, - "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==", + "is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "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" - } - } + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.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=", + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, "requires": { - "base16": "^1.0.0", - "lodash.curry": "^4.0.1", - "lodash.flow": "^3.3.0", - "pure-color": "^1.2.0" + "has-bigints": "^1.0.1" } }, - "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==", + "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": { - "@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" + "binary-extensions": "^2.0.0" } }, - "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==", + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "requires": { - "object-assign": "^4.1.1", - "react-is-deprecated": "^0.1.2", - "shallowequal": "^1.1.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.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 - } - } + "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 }, - "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, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.6" + "hasown": "^2.0.2" } }, - "react-draggable": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.2.tgz", - "integrity": "sha512-zLQs4R4bnBCGnCVTZiD8hPsHtkiJxgMpGDlRESM+EHQo8ysXhKJ2GKdJ8UxxLJdRVceX1j19jy+hQS2wHislPQ==", + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, "requires": { - "classnames": "^2.2.5", - "prop-types": "^15.6.0" + "is-typed-array": "^1.1.13" } }, - "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==", + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "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 - } + "has-tostringtag": "^1.0.0" } }, - "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=", + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "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" - } + "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 }, - "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==", + "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 }, - "react-markdown": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-4.3.1.tgz", - "integrity": "sha512-HQlWFTbDxTtNY6bjgp3C3uv1h2xcjCSi1zAEzfBW9OwJJvENSYiLXWNXN5hHLsoqai7RnZiiHzcnWdXk2Splzw==", + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "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" + "has-tostringtag": "^1.0.0" } }, - "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==", + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "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 - } + "is-extglob": "^2.1.1" } }, - "react-move": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/react-move/-/react-move-2.9.1.tgz", - "integrity": "sha512-5qKYsJrKKpSypEaaYyR2HBbBgX65htRqKDa8o5OGDkq2VfklmTCbLawtYFpdmcJRqbz4jCYpzo2Rrsazq9HA8Q==", + "is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", "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" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" } }, - "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" - } - } - } + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true }, - "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 - } - } + "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 }, - "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" - } + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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 }, - "react-svgmt": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/react-svgmt/-/react-svgmt-1.1.8.tgz", - "integrity": "sha512-3xu7iWuHIbqM2hv4eMsAN1mZKz6EnXTPAcE4mMX/NwuYY5uUKJBoKDAmxY+I6KXHx2SpYJtKAqe1a5jEehteZg==", + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dev": true, "requires": { - "d3-ease": "^1.0.3", - "react-motion": "^0.5.2", - "react-move": "^2.7.0" + "has-tostringtag": "^1.0.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==", + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "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": { - "classnames": "^2.2.5" + "isobject": "^3.0.1" } }, - "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==", + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "requires": { - "classnames": "^2.2.6", - "emotion": "^9.2.3", - "uniqid": "^5.0.3" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, - "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==", + "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": { - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "react-is": "^16.8.6", - "scheduler": "^0.13.6" + "is-unc-path": "^1.0.0" } }, - "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" - } + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true }, - "react-virtualized": { - "version": "9.21.1", - "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.1.tgz", - "integrity": "sha512-E53vFjRRMCyUTEKuDLuGH1ld/9TFzjf/fFW816PE4HFXWZorESbSTYtiZz1oAjra0MminaUU1EnvUxoGuEFFPA==", + "is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "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" + "call-bind": "^1.0.7" } }, - "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" - } + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true }, - "read": { + "is-string": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "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 - } + "has-tostringtag": "^1.0.0" } }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "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 - } + "has-symbols": "^1.0.2" } }, - "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=", + "is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "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" - } - } + "which-typed-array": "^1.1.16" } }, - "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" - } + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "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": { - "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" - } - } + "unc-path-regex": "^0.1.2" } }, - "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" - } + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true }, - "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" - } - } - } + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true }, - "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 - } - } + "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 }, - "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==", + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "requires": { - "balanced-match": "^1.0.0" + "call-bind": "^1.0.2" } }, - "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 - } - } + "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 }, - "redux-logger": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", - "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "requires": { - "deep-diff": "^0.3.5" + "is-docker": "^2.0.0" } }, - "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=", + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "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" - } + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, - "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==" + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true }, - "regenerator-transform": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz", - "integrity": "sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==", + "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": { - "private": "^0.1.6" + "append-transform": "^2.0.0" } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, - "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==", + "istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" }, "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==", + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "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" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.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==", + "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": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "aggregate-error": "^3.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.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "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 }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "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": { - "has": "^1.0.3" + "shebang-regex": "^3.0.0" } }, - "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==", + "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 - } - } - }, - "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==", + "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": { - "jsesc": "~0.5.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" }, "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "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.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" + } } } }, - "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=", + "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": { - "isobject": "^2.0.0" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "isarray": "1.0.0" + "ms": "2.1.2" } } } }, - "release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "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" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, - "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==", + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, "requires": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.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=", + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "requires": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" } }, - "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==", + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "requires": { - "@types/webpack": "^4.41.6", - "trash": "^6.1.1" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "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==", + "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": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { - "@types/anymatch": "*", - "@types/node": "*", - "@types/tapable": "*", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "source-map": "^0.6.0" + "has-flag": "^4.0.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=", + "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 }, - "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==", + "js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "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" + "argparse": "^1.0.7", + "esprima": "^4.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=", + "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 - }, - "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=", + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "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": { + "json-buffer": { "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=", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", "dev": true }, - "requirejs": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", - "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "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==", + "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 }, - "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": { + "json-stable-stringify-without-jsonify": { "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=", + "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 }, - "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=", + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "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" - } + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "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" + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "dependencies": { + "jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "dev": true, + "requires": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + } } }, - "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==", + "jsx-ast-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", + "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", "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" + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" } }, - "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==", + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, "requires": { - "glob": "^7.1.3" + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "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" - } + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true }, - "roughjs-es5": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/roughjs-es5/-/roughjs-es5-0.1.0.tgz", - "integrity": "sha512-NMjzoBgSYk8qEYLSxzxytS20sfdQV7zg119FZjFDjIDwaqodFcf7QwzKbqM64VeAYF61qogaPLk3cs8Gb+TqZA==", + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "requires": { - "babel-runtime": "^6.26.0" + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "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=", + "jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "requires": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, + "optional": true, "requires": { - "is-promise": "^2.1.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, - "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=", + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", "dev": true, "requires": { - "aproba": "^1.1.1" + "json-buffer": "3.0.0" } }, - "rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=", + "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 }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "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 }, - "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=", + "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": { - "rx-lite": "*" + "language-subtag-registry": "~0.3.2" } }, - "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", + "dev": true + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, "requires": { - "tslib": "^1.9.0" + "readable-stream": "^2.0.5" } }, - "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=", + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", "dev": true, "requires": { - "ret": "~0.1.10" + "flush-write-stream": "^1.0.2" } }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true }, - "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==", + "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": { - "truncate-utf8-bytes": "^1.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, - "sanitize-html": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.1.tgz", - "integrity": "sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==", + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "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" + "immediate": "~3.0.5" } }, - "sass-loader": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", - "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", + "liftoff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", "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" + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" }, "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "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==", + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dev": true, "requires": { - "xmlchars": "^2.1.1" + "uc.micro": "^1.0.1" } }, - "scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", + "loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" } }, - "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==", + "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": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" + "p-locate": "^4.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" - } + "lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "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=", + "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.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "commander": "~2.8.1" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "graceful-readlink": ">= 1.0.0" + "color-convert": "^2.0.1" } - } - } - }, - "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==", + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "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" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "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=", + "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": { - "asap": "~2.0.3" + "color-name": "~1.1.4" } }, - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "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.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": { - "fbjs": "^0.8.16", - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" + "has-flag": "^4.0.0" } } } }, - "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==", + "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==", "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" - } - } + "js-tokens": "^3.0.0 || ^4.0.0" } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "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, + "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==", "requires": { - "sver-compat": "^1.5.0" + "yallist": "^4.0.0" } }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "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": { - "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" + "semver": "^6.0.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==", + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, - "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", + "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 }, - "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=", + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "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==", + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "dev": true, "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" }, "dependencies": { - "extend-shallow": { + "argparse": { "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" - } + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true } } }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "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 }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "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==", + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "braces": "^3.0.3", + "picomatch": "^2.3.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==", + "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": { - "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 - } + "bn.js": "^4.0.0", + "brorand": "^1.0.1" } }, - "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==", + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "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, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "shebang-regex": "^1.0.0" + "mime-db": "1.52.0" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "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 }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "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==", + "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": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" + "brace-expansion": "^2.0.1" }, "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 + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "requires": { + "balanced-match": "^1.0.0" + } } } }, - "shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true }, - "shortid": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz", - "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==", + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "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==", "dev": true, "requires": { - "nanoid": "^2.0.0" + "minimist": "^1.2.5" } }, - "side-channel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", - "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true, - "requires": { - "es-abstract": "^1.17.0-next.1", - "object-inspect": "^1.7.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==", + "optional": true + }, + "mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "requires": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "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" + "balanced-match": "^1.0.0" } }, - "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==", + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "readdirp": "^4.0.1" } }, - "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==", + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true }, - "is-callable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", - "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { - "has-symbols": "^1.0.1" + "locate-path": "^6.0.0", + "path-exists": "^4.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 - } - } - }, - "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==", + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "requires": { - "mimic-response": "^2.0.0" + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" } }, - "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 + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } }, "has-flag": { "version": "4.0.0", @@ -23070,3937 +23253,3105 @@ "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==", + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "argparse": "^2.0.1" } - } - } - }, - "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=", + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "p-locate": "^5.0.0" } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "brace-expansion": "^2.0.2" } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "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=", + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { - "is-descriptor": "^1.0.0" + "yocto-queue": "^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==", + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "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 + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "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": { - "kind-of": "^6.0.0" + "shebang-regex": "^3.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==", + "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 + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "has-flag": "^4.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==", + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true } } }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "mocha-junit-reporter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.0.2.tgz", + "integrity": "sha512-vYwWq5hh3v1lG0gdQCBxwNipBfvDiAM1PHroQRNp96+2l72e9wEUTw+mzoK+O0SudgfQ7WvTQZ9Nh3qkAYAjfg==", "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" - } - } + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^6.0.1", + "xml": "^1.0.0" } }, - "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==", + "mocha-multi-reporters": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz", + "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==", "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" + "debug": "^4.1.1", + "lodash": "^4.17.15" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } } } }, - "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==", + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, + "mrmime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", + "integrity": "sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ==", "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 - } - } - } - } + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "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==" + }, + "nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "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==", + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", "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 - } - } + "optional": true }, - "sockjs-client": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz", - "integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=", + "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.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "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 + }, + "nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", "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" + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" } }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "node-abi": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", "dev": true, + "optional": true, "requires": { - "is-plain-obj": "^1.0.0" + "semver": "^7.3.5" } }, - "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=", + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "optional": true + }, + "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": { - "sort-keys": "^1.0.0" + "fs-walk": "0.0.1" } }, - "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==", + "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": { - "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" + "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" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } } }, - "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==", + "node-loader": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-1.0.3.tgz", + "integrity": "sha512-8c9ef5q24F0AjrPxUjdX7qdTlsU1zZCPeqYvSBCH1TJko3QW4qu1uA1C9KbOPdaRQwREDdbSYZgltBAlbV7l5g==", "dev": true, "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.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==", + "node-polyfill-webpack-plugin": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-1.1.4.tgz", + "integrity": "sha512-Z0XTKj1wRWO8o/Vjobsw5iOJCN+Sua3EZEUc2Ziy9CyVvmHKu6o+t4gUH9GOE0czyPR94LI6ZCV/PpcM8b5yow==", "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" + "assert": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "console-browserify": "^1.2.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.12.0", + "domain-browser": "^4.19.0", + "events": "^3.3.0", + "filter-obj": "^2.0.2", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "punycode": "^2.1.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.3.0", + "timers-browserify": "^2.0.12", + "tty-browserify": "^0.0.1", + "url": "^0.11.0", + "util": "^0.12.4", + "vm-browserify": "^1.1.2" }, "dependencies": { - "make-dir": { + "assert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", + "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "dev": true, + "requires": { + "es6-object-assign": "^1.1.0", + "is-nan": "^1.2.1", + "object-is": "^1.0.1", + "util": "^0.12.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true + }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "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" + } + }, + "stream-browserify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", "dev": true, "requires": { - "semver": "^6.0.0" + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" } }, - "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", "dev": true, "requires": { - "glob": "^7.1.3" + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", "dev": true }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", "dev": true, "requires": { - "isexe": "^2.0.0" + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" } } } }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "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": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "process-on-spawn": "^1.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==", + "node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "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" - } + "node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" }, - "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==", + "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 }, - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "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": { - "through": "2" + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" } }, - "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==", + "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": { - "extend-shallow": "^3.0.0" + "once": "^1.3.2" } }, - "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=", + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "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" + "path-key": "^3.0.0" + }, + "dependencies": { + "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 + } } }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", "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" + "boolbase": "^1.0.0" } }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" + "@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", + "get-package-type": "^0.1.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.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "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", + "yargs": "^15.0.2" }, "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=", + "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": { - "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" + "aggregate-error": "^3.0.0" } } } }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "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 }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "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=", + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", "dev": true, "requires": { - "duplexer": "~0.1.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, - "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" - } + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, - "stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "requires": { - "stubs": "^3.0.0" + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" } }, - "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==", + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "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" - } - } + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.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==", + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", "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" - } - } + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" } }, - "streamifier": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", - "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", - "dev": true + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } }, - "streamroller": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.3.tgz", - "integrity": "sha512-AegmvQsscTRhHVO46PhCDerjIpxi7E+d2GxgUDu+nzw/HuLnUdxHWr6WQ+mVn/4iJgMKKFFdiUwFcFRDvcjCtw==", + "object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, "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" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" } }, - "strict-uri-encode": { + "object.hasown": { "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==" + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", + "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } }, - "string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", - "dev": true + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "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" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.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==", + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "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.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 - } + "mimic-fn": "^2.1.0" } }, - "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=", + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.0", - "function-bind": "^1.0.2" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" } }, - "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==", + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "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 - } + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" } }, - "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==", + "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": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "readable-stream": "^2.0.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==", + "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-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-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": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "p-timeout": "^2.0.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==", + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true + }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", + "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": { - "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 - } + "p-try": "^2.0.0" } }, - "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "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": { - "safe-buffer": "~5.1.0" + "p-limit": "^2.2.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==", + "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": { - "ansi-regex": "^4.1.0" + "aggregate-error": "^3.0.0" } }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "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": { - "is-utf8": "^0.2.0" + "p-finally": "^1.0.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=", + "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 }, - "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==", + "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": { - "is-natural-number": "^4.0.1" + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "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=", + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true }, - "strip-outer": { + "parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "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.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "dev": true, + "requires": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "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 + } + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.2" + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" } }, - "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "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==", + "parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^1.0.0" + "semver": "^5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } } }, - "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==", + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", "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" + "parse5": "^6.0.1" }, "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==", + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true } } }, - "stylis": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", + "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 }, - "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==", + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", "dev": true }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "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 + }, + "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-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.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, "requires": { - "minimist": "^1.1.0" + "path-root-regex": "^0.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==" + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "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" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.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" - } - }, - "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==", + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "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" - } + "path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true }, - "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" - } + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, - "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==", + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "requires": { - "loader-utils": "^0.2.11", - "object-assign": "^4.0.1", - "simple-html-tokenizer": "^0.1.1" + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.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=", + "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 - }, - "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" - } + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true }, - "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" - } + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true }, - "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" - } + "picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true }, - "svgpath": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.2.2.tgz", - "integrity": "sha512-7cXFbkZvPkZpKLC+3QIfyUd3/Un/CvJONjTD3Gz5qLuEa73StPOt8kZjTi9apxO6zwCaza0bPNnmzTyrQ4qQlw==", + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "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==", + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "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==", + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, "requires": { - "http-response-object": "^3.0.1", - "sync-rpc": "^1.2.1", - "then-request": "^6.0.0" + "pinkie": "^2.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==", + "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": { - "get-port": "^3.1.0" + "find-up": "^4.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==", + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "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.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" - } - }, - "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" - } - } + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" } }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", "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 - } - } + "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 }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, + "optional": 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" + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.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==", + "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, + "optional": true, "requires": { - "safe-buffer": "~5.1.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } } } }, - "tas-client": { - "version": "0.0.875", - "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.0.875.tgz", - "integrity": "sha512-Y375pAWdOAFKAs2gZHVC3SAxGp8vHNRTpl7W6rBaB8YgZbAX0h0NHUubqHtyuNwH6VF9qy2ckagsuXZP0JignQ==", + "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 + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true + }, + "prettier": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz", + "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==", + "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 + }, + "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": { - "axios": "^0.19.0" + "fromentries": "^1.2.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==", + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "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" - } - } + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.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==", + "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": { - "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 - } - } - } + "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" } }, - "terser": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", - "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "terser-webpack-plugin": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.2.tgz", - "integrity": "sha512-SmvB/6gtEPv+CJ88MH5zDOsZdKXPS/Uzv2//e90+wM1IHFUhsguPKEILgzqrM1nQ4acRXN/SV4Obr55SXC+0oA==", + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "cacache": "^13.0.1", - "find-cache-dir": "^3.2.0", - "jest-worker": "^24.9.0", - "schema-utils": "^2.6.1", - "serialize-javascript": "^2.1.2", - "source-map": "^0.6.1", - "terser": "^4.4.3", - "webpack-sources": "^1.4.3" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "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" - } - }, - "cacache": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", - "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", - "dev": true, - "requires": { - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "minipass": "^3.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "p-map": "^3.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^2.7.1", - "ssri": "^7.0.0", - "unique-filename": "^1.1.1" - } - }, - "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" - } - }, - "fs-minipass": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.0.0.tgz", - "integrity": "sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "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" - } - }, - "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" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "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" - } - }, - "minipass": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", - "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", - "dev": true, - "requires": { - "yallist": "^4.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" - } - }, - "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" - } - }, - "schema-utils": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.2.tgz", - "integrity": "sha512-sazKNMBX/jwrXRkOI7N6dtiTVYqzSckzol8SGuHt0lE/v3xSW6cUkOqzu6Bq2tW+dlUzq3CWIqHU3ZKauliqdg==", - "dev": true, - "requires": { - "ajv": "^6.10.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 - }, - "ssri": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz", - "integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "minipass": "^3.1.1" - } - }, - "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 - } + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" } }, - "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==", + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "side-channel": "^1.1.0" } }, - "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==" + "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" + } }, - "text-table": { + "querystring": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "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 }, - "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==", + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "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" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": 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==", + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, - "requires": { - "asap": "~2.0.6" - } + "optional": true } } }, - "thread-loader": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-2.1.3.tgz", - "integrity": "sha512-wNrVKH2Lcf8ZrWxDF/khdlLlsTMczdcwPA9VEK4c2exlEPynYWxi9op3nPTo5lAnDIkE0rQEB3VBP+4Zncc9Hg==", + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", "dev": true, "requires": { - "loader-runner": "^2.3.1", - "loader-utils": "^1.1.0", - "neo-async": "^2.6.0" + "mute-stream": "~0.0.4" } }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" - }, - "through": { + "readable-stream": { "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==", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "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": { - "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==", + "dev": true, "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==", + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" + "picomatch": "^2.2.1" } }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } }, - "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 + "reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, - "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==", + "regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "requires": { - "setimmediate": "^1.0.4" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" } }, - "timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "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": { - "es5-ext": "~0.10.46", - "next-tick": "1" + "es6-error": "^4.0.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==" + "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" + } }, - "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "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-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 }, - "tinyqueue": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", - "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==", + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", "dev": true }, - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "replace-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", + "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-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", "requires": { - "os-tmpdir": "~1.0.1" + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } }, - "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, + "resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "requires": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^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 + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } }, - "to-arraybuffer": { + "resolve-dir": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } }, - "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==", + "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 }, - "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=", + "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" + } + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "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=", + "rewiremock": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "compare-module-exports": "^2.1.0", + "node-libs-browser": "^2.1.0", + "path-parse": "^1.0.5", + "wipe-node-cache": "^2.1.2", + "wipe-webpack-cache": "^2.1.0" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, "requires": { - "kind-of": "^3.0.2" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" }, "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.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 } } }, - "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==" + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "rxjs-compat": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz", + "integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw==" + }, + "safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "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-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.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=", + "seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "commander": "^2.8.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, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "requires": { - "through2": "^2.0.3" + "lru-cache": "^6.0.0" } }, - "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==", + "semver-greatest-satisfied-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, "requires": { - "commander": "2" + "sver": "^1.8.3" } }, - "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==", + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { - "nopt": "~1.0.10" + "randombytes": "^2.1.0" } }, - "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=" - } - } + "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 }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "requires": { - "punycode": "^2.1.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" } }, - "transform-loader": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/transform-loader/-/transform-loader-0.2.4.tgz", - "integrity": "sha1-5ch4d7qW1R0/IlNoWHtG4ibRzsk=", + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "requires": { - "loader-utils": "^1.0.2" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^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==", + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", "dev": true }, - "trash": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/trash/-/trash-6.1.1.tgz", - "integrity": "sha512-4i56lCmz2RG6WZN018hf4L75L5HboaFuKkHx3wDG/ihevI99e0OgFyl8w6G4ioqBm62V4EJqCy5xw3vQSNXU8A==", + "sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "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" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "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==", + "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 } } }, - "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=", + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.2" + "kind-of": "^6.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=", + "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": { - "utf8-byte-length": "^1.0.1" + "shebang-regex": "^1.0.0" } }, - "tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "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 }, - "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" - } + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, - "ts-mockito": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.5.0.tgz", - "integrity": "sha512-b3qUeMfghRq5k5jw3xNJcnU9RKhqKnRn0k9v9QkN+YpuawrFuMIiGwzFZCpdi5MHy26o7YPnK8gag2awURl3nA==", + "shortid": { + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", "dev": true, "requires": { - "lodash": "^4.17.5" + "nanoid": "^3.3.8" } }, - "ts-node": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", - "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "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 - } + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" } }, - "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==", + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "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 - } + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" } }, - "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==", + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "requires": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "tsconfig-paths": "^3.4.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" } }, - "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==", + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "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": { - "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 - } + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.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==", + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "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==", + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "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" - } - } - } + "optional": true }, - "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==", + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "optional": true, "requires": { - "tsutils": "^2.27.2 <2.29.0" + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" }, "dependencies": { - "tsutils": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", - "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, "requires": { - "tslib": "^1.8.1" + "mimic-response": "^3.1.0" } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "optional": true } } }, - "tslint-plugin-prettier": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-2.1.0.tgz", - "integrity": "sha512-nMCpU+QSpXtydcWXeZF+3ljIbG/K8SHVZwB7K/MtuoQQFXxXN6watqTSBpVXCInuPFvmjiWkhxeMoUW4N0zgSg==", + "sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", "dev": true, "requires": { - "eslint-plugin-prettier": "^2.2.0", - "lines-and-columns": "^1.1.6", - "tslib": "^1.7.1" + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" }, "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==", + "diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "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.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": { - "fast-diff": "^1.1.1", - "jest-docblock": "^21.0.0" + "has-flag": "^4.0.0" } } } }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", "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" + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" } }, - "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==", + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "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==", + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", "dev": true, "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "is-plain-obj": "^1.0.0" } }, - "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=", + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dev": true, "requires": { - "@types/react": "^0.14.44" + "sort-keys": "^1.0.0" }, "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 + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } } } }, - "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==", + "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-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "requires": { - "tunnel": "0.0.4", - "underscore": "1.8.3" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "typed-styles": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", - "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==", + "sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", "dev": true }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "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" + } }, - "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==", + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==" + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "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": { - "is-typedarray": "^1.0.0" + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" } }, - "typemoq": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", - "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", "dev": true, "requires": { - "circular-json": "^0.3.1", - "lodash": "^4.17.4", - "postinstall-build": "^5.0.1" + "streamx": "^2.13.2" } }, - "typescript": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", - "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "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 }, - "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==", + "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": { - "commandpost": "^1.0.0", - "editorconfig": "^0.15.0" + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" } }, - "typestyle": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typestyle/-/typestyle-2.1.0.tgz", - "integrity": "sha512-6uCYPdG4xWLeEcl9O0GtNFnNGhami+irKiLsXSuvWHC/aTS7wdj49WeikWAKN+xHN3b1hm+9v0svwwgSBhCsNA==", + "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 + }, + "streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", "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 - } + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" } }, - "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==", + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "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==", + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "requires": { - "commander": "~2.19.0", - "source-map": "~0.6.1" + "safe-buffer": "~5.2.0" }, "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "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 } } }, - "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==", + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "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==", + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "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" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", - "dev": true + "string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } }, - "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==", + "string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" } }, - "unicode": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz", - "integrity": "sha1-5dUcHbk7bHGguHngsMSvfm/faI4=" + "string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } }, - "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 + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } }, - "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==", + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" + "ansi-regex": "^5.0.1" } }, - "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 + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } }, - "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==", + "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": { - "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" - } - } + "is-natural-number": "^4.0.1" } }, - "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==", + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "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 }, - "unicode-trie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz", - "integrity": "sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=", + "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": { - "pako": "^0.2.5", - "tiny-inflate": "^1.0.0" + "escape-string-regexp": "^1.0.2" } }, - "unified": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", - "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==" + }, + "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": { - "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" + "has-flag": "^3.0.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==", + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "optional": true + } } }, - "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==", + "tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "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==", + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, + "optional": true, "requires": { - "unique-slug": "^2.0.0" + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "optional": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.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, + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "optional": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } } }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "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": { - "imurmurhash": "^0.1.4" + "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" } }, - "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==", + "tas-client": { + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", + "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "requires": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" + "streamx": "^2.12.5" } }, - "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==", + "terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "requires": { - "unist-util-visit": "^1.1.0" + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" } }, - "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==", + "terser-webpack-plugin": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "requires": { - "unist-util-visit-parents": "^2.0.0" + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "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==", + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "requires": { - "unist-util-is": "^3.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } - } - } - }, - "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=", + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "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" - } - } + "fast-deep-equal": "^3.1.3" } }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } } } }, - "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=", + "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": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" }, "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, - "url-join": { + "text-decoder": { "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==", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", "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" + "b4a": "^1.6.4" } }, - "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=", + "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 }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "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==", + "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": { - "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 - } + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "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==", + "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": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" + "through2": "~2.0.0", + "xtend": "~4.0.0" } }, - "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==", + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", "dev": true }, - "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", "dev": true, "requires": { - "homedir-polyfill": "^1.0.1" + "setimmediate": "^1.0.4" } }, - "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==", + "tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==" + }, + "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": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.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==", + "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 }, - "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==", + "to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "requires": { - "d3-array": "^2.0.3", - "vega-dataflow": "^5.1.0", - "vega-util": "^1.8.0" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" }, "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==", + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "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 } } }, - "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==", + "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": { - "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 - } + "is-number": "^7.0.0" } }, - "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==", + "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": { - "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 - } + "through2": "^2.0.3" } }, - "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==", + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "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==", + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, "requires": { - "vega-util": "^1.11.0" + "escape-string-regexp": "^1.0.2" } }, - "vega-force": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-4.0.3.tgz", - "integrity": "sha512-4stItN4jD9H1CENaCz4jXRNS1Bi9cozMOUjX2824FeJENi2RZSiAZAaGbscgerZQ/jbNcOHD8PHpC2pWldEvGA==", + "ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "requires": {} + }, + "ts-loader": { + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", + "integrity": "sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==", "dev": true, "requires": { - "d3-force": "^2.0.1", - "vega-dataflow": "^5.4.0", - "vega-util": "^1.11.0" + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" }, "dependencies": { - "d3-force": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { "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==", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" + "color-name": "~1.1.4" } - } - } - }, - "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==", + "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 }, - "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==", + "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.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" + } } } }, - "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==", + "ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", "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 - } + "lodash": "^4.17.5" } }, - "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" + "ts-node": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" } }, - "vega-lite": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-3.4.0.tgz", - "integrity": "sha512-RJg9uBNh5g0hA8xTzAcALUfNx0cEq7E7xx+vxPEGSMgI8z+A5KlE9u4jUx6nKu7Mjg1qZO8WOyWCmBS1kdFWPg==", + "tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "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" + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, "dependencies": { - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "minimist": "^1.2.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==", + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "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==", + } + } + }, + "tsconfig-paths-webpack-plugin": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz", + "integrity": "sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "color-convert": "^2.0.1" } }, - "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==", + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "vega-util": "^1.10.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.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==", + "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": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "color-name": "~1.1.4" } }, - "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" - } + "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 }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "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.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": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "has-flag": "^4.0.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==", + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "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.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "optional": 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 - } + "safe-buffer": "^5.0.1" + } + }, + "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-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 + }, + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } + }, + "typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "requires": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "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" } }, - "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" - } + "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": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "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 + }, + "uint64be": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-3.0.0.tgz", + "integrity": "sha512-mliiCSrsE29aNBI7O9W5gGv6WmA9kBR8PtTt6Apaxns076IRdYrrtFhXHEWMj5CSum3U7cv7/pi4xmi4XsIOqg==" }, - "vega-projection": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-1.3.0.tgz", - "integrity": "sha512-BFOc/XSVVW96WIAAyiUcppCeegniibiKGX0OLbGpQ5WIbeDHsbCXqnkeBpD5wsjvPXaiQRHTZ0PZ8VvCoCQV+g==", + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, "requires": { - "d3-geo": "^1.11.6" + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" } }, - "vega-regression": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-1.0.1.tgz", - "integrity": "sha512-eeQnLccWHAs2rovu2x3G50reF3Die9QoUGy/dMAO6sbDDA7B5s5qW3uq1NNnG93l3Ch84lO71qytxDBTdaQThA==", + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "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 - } + "buffer": "^5.2.1", + "through": "^2.3.8" } }, - "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" - } + "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 }, - "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==", + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, + "undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "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" + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.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==", + "fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", "dev": true, "requires": { - "d3-array": "1.2.0 - 2", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" + "fastest-levenshtein": "^1.0.7" } - }, - "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 - } - } + "undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "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==", + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "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==", + "unicode": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz", + "integrity": "sha512-BjinxTXkbm9Jomp/YBTMGusr4fxIG67fNGShHIRAL16Ur2GJTq2xvLi+sxuiJmInCmwqqev2BCFKyvbfp/yAkg==" + }, + "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": { - "vega-expression": "^2.6.1", - "vega-util": "^1.11.0" + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.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==", + "update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "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 - } + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, - "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==", + "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": { - "vega-util": "^1.10.0" + "punycode": "^2.1.0" } }, - "vega-transforms": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.4.3.tgz", - "integrity": "sha512-8GxMm1CecRzNG89zr4JroOfPsWCkb9lWL5owSBsB+AalyJaFjxYwKuCM6L2OsAKID3su3aeQ0oSBQTR3v0nqPA==", + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", "dev": true, "requires": { - "d3-array": "^2.3.2", - "vega-dataflow": "^5.4.1", - "vega-statistics": "^1.6.1", - "vega-util": "^1.12.0" + "punycode": "1.3.2", + "querystring": "0.2.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==", + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", "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==", + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "dev": true, "requires": { - "vega-util": "^1.11.0" + "prepend-http": "^2.0.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==", + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", "dev": true }, - "vega-view": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.3.1.tgz", - "integrity": "sha512-pKbpmR1uz9YOsqocx6KWEHD1EwwpOVIfRH6h8g5xQLiUQP37bohfKZ2vL05MQCupXN2dNYdp51iyETbMJMj7nQ==", + "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": { - "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" + "inherits": "2.0.3" }, "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==", + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "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" - } + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, - "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" - } + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, - "vfile-location": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", - "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", + "v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "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" - } + "v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true }, - "viewport-dimensions": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz", - "integrity": "sha1-3nQHR9tTh/0XJfUXXpG6x2r982w=", + "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 }, "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", "dev": true, "requires": { "clone": "^2.1.1", @@ -27009,13 +26360,68 @@ "cloneable-readable": "^1.0.0", "remove-trailing-separator": "^1.0.1", "replace-ext": "^1.0.0" + } + }, + "vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "requires": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" }, "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } } } }, @@ -27042,38 +26448,6 @@ "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": { @@ -27102,597 +26476,269 @@ } } }, - "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==", + "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 }, - "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" - } + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.35.0.tgz", + "integrity": "sha512-+OMm11R1bGYbpIJ5eQIkwoDGFF4GvBz3Ztl6/VM+/RNNb2Gjk2c0Ku+oMmfhlTmTlPCpgHBsH4JqVCbUYhu5bA==" }, "vscode-jsonrpc": { - "version": "6.0.0-next.3", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.3.tgz", - "integrity": "sha512-bkAtRMXbTwU1pAXnlbkAMnrqXZX3q2/zttkcnPZOsDqgoycO2L7GK2aN3K5qCMZmhItwBU185T78Q9YH0v5jEA==" + "version": "9.0.0-next.5", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.5.tgz", + "integrity": "sha512-Sl/8RAJtfF/2x/TPBVRuhzRAcqYR/QDjEjNqMcoKFfqsxfVUPzikupRDQYB77Gkbt1RrW43sSuZ5uLtNAcikQQ==" }, "vscode-languageclient": { - "version": "7.0.0-next.6", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0-next.6.tgz", - "integrity": "sha512-3CPGOXCSxpPGONNVg+ZhY4AYyJrjLDJSKJT2acJ6L5mSlzKACFkFd3yQEGVHS/O0v4n/9i/KRNpBGVSR8ngw7Q==", + "version": "10.0.0-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.12.tgz", + "integrity": "sha512-q7cVYCcYiv+a+fJYCbjMMScOGBnX162IBeUMFg31mvnN7RHKx5/CwKaCz+r+RciJrRXMqS8y8qpEVGgeIPnbxg==", "requires": { - "semver": "^6.3.0", - "vscode-languageserver-protocol": "3.16.0-next.5" + "minimatch": "^9.0.3", + "semver": "^7.6.0", + "vscode-languageserver-protocol": "3.17.6-next.10" }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "vscode-jsonrpc": { - "version": "6.0.0-next.3", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.3.tgz", - "integrity": "sha512-bkAtRMXbTwU1pAXnlbkAMnrqXZX3q2/zttkcnPZOsDqgoycO2L7GK2aN3K5qCMZmhItwBU185T78Q9YH0v5jEA==" - }, - "vscode-languageserver-protocol": { - "version": "3.16.0-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.5.tgz", - "integrity": "sha512-MVuLT4d5tdDo/14laViyZY+p1HT7wYDwJdvGmo8mCfkgON5c10AABhtqjD+LccidBcwdbKy+4dwwUUxiNg/8PA==", + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "requires": { - "vscode-jsonrpc": "6.0.0-next.3", - "vscode-languageserver-types": "3.16.0-next.2" + "balanced-match": "^1.0.0" } }, - "vscode-languageserver-types": { - "version": "3.16.0-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz", - "integrity": "sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==" - } - } - }, - "vscode-languageserver": { - "version": "7.0.0-next.4", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0-next.4.tgz", - "integrity": "sha512-yyFMuKLNkSKYb//6dQHCo0wlDYWkgRPEXoV+/aYXHp9ziIBqQxKJJFrf/rnXYReCz1TroxYMcFGdO5UrBsLAaA==", - "requires": { - "vscode-languageserver-protocol": "3.16.0-next.5" - }, - "dependencies": { - "vscode-jsonrpc": { - "version": "6.0.0-next.3", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.3.tgz", - "integrity": "sha512-bkAtRMXbTwU1pAXnlbkAMnrqXZX3q2/zttkcnPZOsDqgoycO2L7GK2aN3K5qCMZmhItwBU185T78Q9YH0v5jEA==" - }, - "vscode-languageserver-protocol": { - "version": "3.16.0-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.5.tgz", - "integrity": "sha512-MVuLT4d5tdDo/14laViyZY+p1HT7wYDwJdvGmo8mCfkgON5c10AABhtqjD+LccidBcwdbKy+4dwwUUxiNg/8PA==", + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "requires": { - "vscode-jsonrpc": "6.0.0-next.3", - "vscode-languageserver-types": "3.16.0-next.2" + "brace-expansion": "^2.0.2" } - }, - "vscode-languageserver-types": { - "version": "3.16.0-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz", - "integrity": "sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==" } } }, "vscode-languageserver-protocol": { - "version": "3.16.0-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.5.tgz", - "integrity": "sha512-MVuLT4d5tdDo/14laViyZY+p1HT7wYDwJdvGmo8mCfkgON5c10AABhtqjD+LccidBcwdbKy+4dwwUUxiNg/8PA==", + "version": "3.17.6-next.10", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.10.tgz", + "integrity": "sha512-KOrrWn4NVC5jnFC5N6y/fyNKtx8rVYr67lhL/Z0P4ZBAN27aBsCnLBWAMIkYyJ1K8EZaE5r7gqdxrS9JPB6LIg==", "requires": { - "vscode-jsonrpc": "6.0.0-next.3", - "vscode-languageserver-types": "3.16.0-next.2" - }, - "dependencies": { - "vscode-jsonrpc": { - "version": "6.0.0-next.3", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.3.tgz", - "integrity": "sha512-bkAtRMXbTwU1pAXnlbkAMnrqXZX3q2/zttkcnPZOsDqgoycO2L7GK2aN3K5qCMZmhItwBU185T78Q9YH0v5jEA==" - } + "vscode-jsonrpc": "9.0.0-next.5", + "vscode-languageserver-types": "3.17.6-next.5" } }, "vscode-languageserver-types": { - "version": "3.16.0-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz", - "integrity": "sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==" + "version": "3.17.6-next.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", + "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" }, "vscode-tas-client": { - "version": "0.0.864", - "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.0.864.tgz", - "integrity": "sha512-mRMpeTVQ8Rx3p4yF9y8AABanzbqtLRdJA99dzeQ9MdIHsSEdp0kEwxqayzDhNHDdp8vNbQkHN8zMxSvm/ZWdpg==", - "requires": { - "tas-client": "0.0.875" - } - }, - "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, + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz", + "integrity": "sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w==", "requires": { - "loose-envify": "^1.0.0" + "tas-client": "0.2.33" } }, "watchpack": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", - "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "requires": { - "chokidar": "^2.1.8", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" } }, - "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==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } }, - "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==", + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "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": { - "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" - } - } + "fast-deep-equal": "^3.1.3" } }, - "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==", + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "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==", + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "requires": { - "duplexer": "^0.1.1", - "pify": "^4.0.1" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" } } } }, - "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==", + "webpack-bundle-analyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", + "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", "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" + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" }, "dependencies": { - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "color-convert": "^2.0.1" } }, - "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==", + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "global-prefix": "^3.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.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==", + "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": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" + "color-name": "~1.1.4" } }, - "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==", + "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 }, - "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=", + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "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==", + "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 }, - "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==", + "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": "^3.0.0" + "has-flag": "^4.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=", + } + } + }, + "webpack-cli": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", + "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.1.1", + "@webpack-cli/info": "^1.4.1", + "@webpack-cli/serve": "^1.6.1", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "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" - } + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "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==", + "rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", "dev": true, "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "resolve": "^1.9.0" } } } @@ -27704,181 +26750,85 @@ "dev": true }, "webpack-merge": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.1.tgz", - "integrity": "sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, "requires": { - "lodash": "^4.17.5" + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" } }, "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==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", "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==", + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/webpack-require-from/-/webpack-require-from-1.8.6.tgz", + "integrity": "sha512-QmRsOkOYPKeNXp4uVc7qxnPrFQPrP4bhOc/gl4QenTFNgXdEbF1U8VC+jM/Sljb0VzJLNgyNiHlVkuHjcmDtBQ==", "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 + "requires": {} }, - "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==", + "webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "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, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "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==", + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, "requires": { - "stackback": "0.0.2" + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "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" - } - } + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" } }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=" + }, "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==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.2.tgz", + "integrity": "sha512-m7NXa8qSxBGMtdQilOu53ctMaIBXy93FOP04EC1Uf4bpsE+r+adfLKwIMIvGbABsznaSNxK/ErD4xXDyY5og9w==", "dev": true }, "wipe-webpack-cache": { @@ -27890,56 +26840,93 @@ "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==", + "worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", "dev": true, "requires": { - "microevent.ts": "~0.1.1" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" } }, "workerpool": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", - "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "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=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.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=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "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 + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "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": { - "ansi-regex": "^2.0.0" + "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 } } }, @@ -27948,15 +26935,6 @@ "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", @@ -27970,54 +26948,11 @@ } }, "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==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "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 - } - } + "requires": {} }, "xml": { "version": "1.0.1", @@ -28025,66 +26960,36 @@ "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==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "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": "~11.0.0" } }, "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 + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "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==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "15.3.1", @@ -28105,12 +27010,6 @@ "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", @@ -28147,84 +27046,12 @@ "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", @@ -28255,117 +27082,40 @@ } }, "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 - } - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true }, "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.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" - } - }, - "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=", + "camelcase": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "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==", + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "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=", + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "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" - } - }, - "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" - } } } }, @@ -28388,68 +27138,17 @@ "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==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "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" - } - } - } + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/package.datascience-ui.dependencies.json b/package.datascience-ui.dependencies.json deleted file mode 100644 index c42689044d43..000000000000 --- a/package.datascience-ui.dependencies.json +++ /dev/null @@ -1,307 +0,0 @@ -[ - "@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 b48a3c400202..2a27cddc0976 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,33 @@ { "name": "python", "displayName": "Python", - "description": "Linting, Debugging (multi-threaded, remote), Intellisense, Jupyter Notebooks, code formatting, refactoring, unit tests, snippets, and more.", - "version": "2020.8.0-dev", + "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", + "version": "2026.5.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, - "languageServerVersion": "0.5.30", + "capabilities": { + "untrustedWorkspaces": { + "supported": false, + "description": "The Python extension is not available in untrusted workspaces. Use Pylance to get partial IntelliSense support for Python files." + }, + "virtualWorkspaces": { + "supported": "limited", + "description": "Only Partial IntelliSense supported." + } + }, "publisher": "ms-python", - "enableProposedApi": true, + "enabledApiProposals": [ + "contribEditorContentMenu", + "quickPickSortByLabel", + "testObserver", + "quickPickItemTooltip", + "terminalDataWriteEvent", + "terminalExecuteCommandEvent", + "codeActionAI", + "notebookReplDocument", + "notebookVariableProvider" + ], "author": { "name": "Microsoft Corporation" }, @@ -21,15 +40,16 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", + "qna": "https://github.com/microsoft/vscode-python/discussions/categories/q-a", "icon": "icon.png", "galleryBanner": { "color": "#1e415e", "theme": "dark" }, "engines": { - "vscode": "^1.47.0" + "vscode": "^1.95.0" }, + "enableTelemetry": false, "keywords": [ "python", "django", @@ -39,3238 +59,1438 @@ "categories": [ "Programming Languages", "Debuggers", - "Linters", - "Snippets", - "Formatters", "Other", - "Extension Packs", "Data Science", - "Machine Learning", - "Notebooks" + "Machine Learning" ], "activationEvents": [ + "onDebugInitialConfigurations", "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", + "onCommand:python.copilotSetupTests", "workspaceContains:mspythonconfig.json", - "workspaceContains:pyproject.toml" + "workspaceContains:pyproject.toml", + "workspaceContains:Pipfile", + "workspaceContains:setup.py", + "workspaceContains:requirements.txt", + "workspaceContains:pylock.toml", + "workspaceContains:**/pylock.*.toml", + "workspaceContains:manage.py", + "workspaceContains:app.py", + "workspaceContains:.venv", + "workspaceContains:.conda", + "onLanguageModelTool:get_python_environment_details", + "onLanguageModelTool:get_python_executable_details", + "onLanguageModelTool:install_python_packages", + "onLanguageModelTool:configure_python_environment", + "onLanguageModelTool:create_virtual_environment", + "onTerminalShellIntegration:python" ], "main": "./out/client/extension", + "browser": "./dist/extension.browser.js", + "l10n": "./l10n", "contributes": { - "snippets": [ + "problemMatchers": [ { - "language": "python", - "path": "./snippets/python.json" + "name": "python", + "owner": "python", + "source": "python", + "fileLocation": "autoDetect", + "pattern": [ + { + "regexp": "^.*File \\\"([^\\\"]|.*)\\\", line (\\d+).*", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*(.*)\\s*$" + }, + { + "regexp": "^\\s*(.*Error.*)$", + "message": 1 + } + ] } ], - "keybindings": [ - { - "command": "python.execSelectionInTerminal", - "key": "shift+enter", - "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !python.datascience.ownsSelection" - }, - { - "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": "notebook.cell.execute", - "key": "shift+enter", - "when": "notebookCellListFocused" - }, - { - "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" - }, + "walkthroughs": [ { - "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" + "id": "pythonWelcome", + "title": "%walkthrough.pythonWelcome.title%", + "description": "%walkthrough.pythonWelcome.description%", + "when": "workspacePlatform != webworker", + "steps": [ + { + "id": "python.createPythonFolder", + "title": "%walkthrough.step.python.createPythonFolder.title%", + "description": "%walkthrough.step.python.createPythonFolder.description%", + "media": { + "svg": "resources/walkthrough/open-folder.svg", + "altText": "%walkthrough.step.python.createPythonFile.altText%" + }, + "when": "workspaceFolderCount = 0" + }, + { + "id": "python.createPythonFile", + "title": "%walkthrough.step.python.createPythonFile.title%", + "description": "%walkthrough.step.python.createPythonFile.description%", + "media": { + "svg": "resources/walkthrough/open-folder.svg", + "altText": "%walkthrough.step.python.createPythonFile.altText%" + } + }, + { + "id": "python.installPythonWin8", + "title": "%walkthrough.step.python.installPythonWin8.title%", + "description": "%walkthrough.step.python.installPythonWin8.description%", + "media": { + "markdown": "resources/walkthrough/install-python-windows-8.md" + }, + "when": "workspacePlatform == windows && showInstallPythonTile" + }, + { + "id": "python.installPythonMac", + "title": "%walkthrough.step.python.installPythonMac.title%", + "description": "%walkthrough.step.python.installPythonMac.description%", + "media": { + "markdown": "resources/walkthrough/install-python-macos.md" + }, + "when": "workspacePlatform == mac && showInstallPythonTile", + "command": "workbench.action.terminal.new" + }, + { + "id": "python.installPythonLinux", + "title": "%walkthrough.step.python.installPythonLinux.title%", + "description": "%walkthrough.step.python.installPythonLinux.description%", + "media": { + "markdown": "resources/walkthrough/install-python-linux.md" + }, + "when": "workspacePlatform == linux && showInstallPythonTile", + "command": "workbench.action.terminal.new" + }, + { + "id": "python.createEnvironment", + "title": "%walkthrough.step.python.createEnvironment.title%", + "description": "%walkthrough.step.python.createEnvironment.description%", + "media": { + "svg": "resources/walkthrough/create-environment.svg", + "altText": "%walkthrough.step.python.createEnvironment.altText%" + } + }, + { + "id": "python.runAndDebug", + "title": "%walkthrough.step.python.runAndDebug.title%", + "description": "%walkthrough.step.python.runAndDebug.description%", + "media": { + "svg": "resources/walkthrough/rundebug2.svg", + "altText": "%walkthrough.step.python.runAndDebug.altText%" + } + }, + { + "id": "python.learnMoreWithDS", + "title": "%walkthrough.step.python.learnMoreWithDS.title%", + "description": "%walkthrough.step.python.learnMoreWithDS.description%", + "media": { + "altText": "%walkthrough.step.python.learnMoreWithDS.altText%", + "svg": "resources/walkthrough/learnmore.svg" + } + } + ] }, { - "mac": "D D", - "win": "D D", - "linux": "D D", - "key": "D D", - "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", - "command": "notebook.cell.delete" - }, + "id": "pythonDataScienceWelcome", + "title": "%walkthrough.pythonDataScienceWelcome.title%", + "description": "%walkthrough.pythonDataScienceWelcome.description%", + "when": "false", + "steps": [ + { + "id": "python.installJupyterExt", + "title": "%walkthrough.step.python.installJupyterExt.title%", + "description": "%walkthrough.step.python.installJupyterExt.description%", + "media": { + "svg": "resources/walkthrough/data-science.svg", + "altText": "%walkthrough.step.python.installJupyterExt.altText%" + } + }, + { + "id": "python.createNewNotebook", + "title": "%walkthrough.step.python.createNewNotebook.title%", + "description": "%walkthrough.step.python.createNewNotebook.description%", + "media": { + "svg": "resources/walkthrough/create-notebook.svg", + "altText": "%walkthrough.step.python.createNewNotebook.altText%" + }, + "completionEvents": [ + "onCommand:jupyter.createnewnotebook", + "onCommand:workbench.action.files.openFolder", + "onCommand:workbench.action.files.openFileFolder" + ] + }, + { + "id": "python.openInteractiveWindow", + "title": "%walkthrough.step.python.openInteractiveWindow.title%", + "description": "%walkthrough.step.python.openInteractiveWindow.description%", + "media": { + "svg": "resources/walkthrough/interactive-window.svg", + "altText": "%walkthrough.step.python.openInteractiveWindow.altText%" + }, + "completionEvents": [ + "onCommand:jupyter.createnewinteractive" + ] + }, + { + "id": "python.dataScienceLearnMore", + "title": "%walkthrough.step.python.dataScienceLearnMore.title%", + "description": "%walkthrough.step.python.dataScienceLearnMore.description%", + "media": { + "svg": "resources/walkthrough/learnmore.svg", + "altText": "%walkthrough.step.python.dataScienceLearnMore.altText%" + } + } + ] + } + ], + "breakpoints": [ { - "mac": "Z", - "win": "Z", - "linux": "Z", - "key": "Z", - "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", - "command": "notebook.undo" + "language": "html" }, { - "mac": "C", - "win": "C", - "linux": "C", - "key": "C", - "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", - "command": "notebook.cell.copy" + "language": "jinja" }, { - "mac": "X", - "win": "X", - "linux": "X", - "key": "X", - "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", - "command": "notebook.cell.cut" + "language": "python" }, { - "mac": "V", - "win": "V", - "linux": "V", - "key": "V", - "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", - "command": "notebook.cell.paste" + "language": "django-html" }, { - "mac": "ctrl+shift+-", - "win": "ctrl+shift+-", - "linux": "ctrl+shift+-", - "when": "editorTextFocus && inputFocus && notebookEditorFocused && notebookViewType == jupyter-notebook", - "command": "notebook.cell.split" + "language": "django-txt" } ], "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%", + "title": "%python.command.python.createNewFile.title%", + "shortTitle": "%python.menu.createNewFile.title%", "category": "Python", - "icon": { - "light": "resources/light/run-tests.svg", - "dark": "resources/dark/run-tests.svg" - } + "command": "python.createNewFile" }, { - "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.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%" }, { - "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.analysis.restartLanguageServer", + "title": "%python.command.python.analysis.restartLanguageServer.title%" }, { - "command": "python.switchToWeeklyChannel", - "title": "%python.command.python.switchToWeeklyChannel.title%", - "category": "Python" + "category": "Python", + "command": "python.clearCacheAndReload", + "title": "%python.command.python.clearCacheAndReload.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" + "title": "%python.command.python.clearWorkspaceInterpreter.title%" }, { - "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.configureTests", + "title": "%python.command.python.configureTests.title%" }, { - "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.createTerminal", + "title": "%python.command.python.createTerminal.title%" }, { - "command": "python.viewLanguageServerOutput", - "title": "%python.command.python.viewLanguageServerOutput.title%", "category": "Python", - "enablement": "python.hasLanguageServerOutputChannel" + "command": "python.createEnvironment", + "title": "%python.command.python.createEnvironment.title%" }, { - "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.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%" }, { - "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.execInTerminal", + "title": "%python.command.python.execInTerminal.title%" }, { - "command": "python.discoverTests", - "title": "%python.command.python.discoverTests.title%", "category": "Python", - "icon": { - "light": "resources/light/refresh.svg", - "dark": "resources/dark/refresh.svg" - } + "command": "python.execInTerminal-icon", + "icon": "$(play)", + "title": "%python.command.python.execInTerminalIcon.title%" }, { - "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.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%" }, { - "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.execSelectionInDjangoShell", + "title": "%python.command.python.execSelectionInDjangoShell.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.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" + "shortTitle": "%python.command.python.execSelectionInTerminal.shortTitle%" }, { - "command": "python.datascience.selectjupyteruri", - "title": "%python.command.python.datascience.selectjupyteruri.title%", "category": "Python", - "when": "python.datascience.featureenabled" + "command": "python.execInREPL", + "title": "%python.command.python.execInREPL.title%" }, { - "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.reportIssue", + "title": "%python.command.python.reportIssue.title%" }, { - "command": "python.datascience.importnotebookfile", - "title": "%python.command.python.datascience.importnotebookfile.title%", - "category": "Python" + "category": "Test", + "command": "testing.reRunFailTests", + "icon": "$(run-errors)", + "title": "%python.command.testing.rerunFailedTests.title%" }, { - "command": "python.datascience.opennotebook", - "title": "%python.command.python.datascience.opennotebook.title%", - "category": "Python" + "category": "Python", + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%" }, { - "command": "python.datascience.opennotebookInPreviewEditor", - "title": "%python.command.python.datascience.opennotebookInPreviewEditor.title%", - "category": "Python" + "category": "Python", + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%" }, { - "command": "python.datascience.exportoutputasnotebook", - "title": "%python.command.python.datascience.exportoutputasnotebook.title%", - "category": "Python" + "category": "Python", + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.title%" }, { - "command": "python.datascience.exportfileasnotebook", - "title": "%python.command.python.datascience.exportfileasnotebook.title%", - "category": "Python" + "category": "Python", + "command": "python.viewLanguageServerOutput", + "enablement": "python.hasLanguageServerOutputChannel", + "title": "%python.command.python.viewLanguageServerOutput.title%" }, { - "command": "python.datascience.exportfileandoutputasnotebook", - "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", - "category": "Python" + "category": "Python", + "command": "python.viewOutput", + "icon": { + "dark": "resources/dark/repl.svg", + "light": "resources/light/repl.svg" + }, + "title": "%python.command.python.viewOutput.title%" }, { - "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.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" + "command": "python.installJupyter", + "title": "%python.command.python.installJupyter.title%" } ], - "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" + "configuration": { + "properties": { + "python.activeStateToolPath": { + "default": "state", + "description": "%python.activeStateToolPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "command": "python.sortImports", - "title": "Refactor: Sort Imports", - "group": "Refactor", - "when": "editorLangId == python && !notebookEditorFocused" + "python.autoComplete.extraPaths": { + "default": [], + "description": "%python.autoComplete.extraPaths.description%", + "scope": "resource", + "type": "array", + "uniqueItems": true }, - { - "command": "python.execSelectionInTerminal", - "group": "Python", - "when": "editorFocus && editorLangId == python" + "python.createEnvironment.contentButton": { + "default": "hide", + "markdownDescription": "%python.createEnvironment.contentButton.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "show", + "hide" + ] }, - { - "command": "python.execSelectionInDjangoShell", - "group": "Python", - "when": "editorHasSelection && editorLangId == python && python.isDjangoProject" + "python.createEnvironment.trigger": { + "default": "prompt", + "markdownDescription": "%python.createEnvironment.trigger.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "off", + "prompt" + ] }, - { - "when": "resourceLangId == python", - "command": "python.execInTerminal", - "group": "Python" + "python.condaPath": { + "default": "", + "description": "%python.condaPath.description%", + "scope": "machine", + "type": "string" }, - { - "when": "resourceLangId == python", - "command": "python.runCurrentTestFile", - "group": "Python" + "python.defaultInterpreterPath": { + "default": "python", + "markdownDescription": "%python.defaultInterpreterPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.runallcells", - "group": "Python2" + "python.envFile": { + "default": "${workspaceFolder}/.env", + "description": "%python.envFile.description%", + "scope": "resource", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.runcurrentcell", - "group": "Python2" + "python.useEnvironmentsExtension": { + "default": false, + "description": "%python.useEnvironmentsExtension.description%", + "scope": "machine-overridable", + "type": "boolean", + "tags": [ + "onExP", + "preview" + ] }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.runcurrentcelladvance", - "group": "Python2" + "python.experiments.enabled": { + "default": true, + "description": "%python.experiments.enabled.description%", + "scope": "window", + "type": "boolean" }, - { - "command": "python.datascience.runFileInteractive", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.featureenabled && !notebookEditorFocused" + "python.experiments.optInto": { + "default": [], + "markdownDescription": "%python.experiments.optInto.description%", + "items": { + "enum": [ + "All", + "pythonSurveyNotification", + "pythonPromptNewToolsExt", + "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", + "pythonTestAdapter" + ], + "enumDescriptions": [ + "%python.experiments.All.description%", + "%python.experiments.pythonSurveyNotification.description%", + "%python.experiments.pythonPromptNewToolsExt.description%", + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", + "%python.experiments.pythonTestAdapter.description%" + ] + }, + "scope": "window", + "type": "array", + "uniqueItems": true }, - { - "command": "python.datascience.runfromline", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.ownsSelection && python.datascience.featureenabled && !notebookEditorFocused" + "python.experiments.optOutFrom": { + "default": [], + "markdownDescription": "%python.experiments.optOutFrom.description%", + "items": { + "enum": [ + "All", + "pythonSurveyNotification", + "pythonPromptNewToolsExt", + "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", + "pythonTestAdapter" + ], + "enumDescriptions": [ + "%python.experiments.All.description%", + "%python.experiments.pythonSurveyNotification.description%", + "%python.experiments.pythonPromptNewToolsExt.description%", + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", + "%python.experiments.pythonTestAdapter.description%" + ] + }, + "scope": "window", + "type": "array", + "uniqueItems": true }, - { - "command": "python.datascience.runtoline", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.ownsSelection && python.datascience.featureenabled && !notebookEditorFocused" + "python.globalModuleInstallation": { + "default": false, + "description": "%python.globalModuleInstallation.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.execSelectionInteractive", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.featureenabled && python.datascience.ownsSelection && !notebookEditorFocused" + "python.languageServer": { + "default": "Default", + "description": "%python.languageServer.description%", + "enum": [ + "Default", + "Jedi", + "Pylance", + "None" + ], + "enumDescriptions": [ + "%python.languageServer.defaultDescription%", + "%python.languageServer.jediDescription%", + "%python.languageServer.pylanceDescription%", + "%python.languageServer.noneDescription%" + ], + "scope": "window", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && resourceLangId == jupyter && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.importnotebook", - "group": "Python3@1" + "python.interpreter.infoVisibility": { + "default": "onPythonRelated", + "description": "%python.interpreter.infoVisibility.description%", + "enum": [ + "never", + "onPythonRelated", + "always" + ], + "enumDescriptions": [ + "%python.interpreter.infoVisibility.never.description%", + "%python.interpreter.infoVisibility.onPythonRelated.description%", + "%python.interpreter.infoVisibility.always.description%" + ], + "scope": "machine", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.exportfileasnotebook", - "group": "Python3@2" + "python.logging.level": { + "default": "error", + "deprecationMessage": "%python.logging.level.deprecation%", + "description": "%python.logging.level.description%", + "enum": [ + "debug", + "error", + "info", + "off", + "warn" + ], + "scope": "machine", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.exportfileandoutputasnotebook", - "group": "Python3@3" - } - ], - "editor/title": [ - { - "command": "python.execInTerminal-icon", - "title": "%python.command.python.execInTerminal.title%", - "group": "navigation", - "when": "resourceLangId == python && python.showPlayIcon" + "python.missingPackage.severity": { + "default": "Hint", + "description": "%python.missingPackage.severity.description%", + "enum": [ + "Error", + "Hint", + "Information", + "Warning" + ], + "scope": "resource", + "type": "string" }, - { - "command": "python.datascience.notebookeditor.restartkernel", - "title": "%python.command.python.datascience.restartkernel.title%", - "group": "navigation", - "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook'" + "python.locator": { + "default": "js", + "description": "%python.locator.description%", + "enum": [ + "js", + "native" + ], + "tags": [ + "onExP", + "preview" + ], + "scope": "machine", + "type": "string" }, - { - "command": "python.datascience.notebookeditor.trust", - "title": "%DataScience.trustNotebookCommandTitle%", - "group": "navigation@1", - "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook' && !python.datascience.isnotebooktrusted && python.datascience.trustfeatureenabled" + "python.pipenvPath": { + "default": "pipenv", + "description": "%python.pipenvPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "command": "python.datascience.export", - "title": "%DataScience.notebookExportAs%", - "group": "navigation", - "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook' && python.datascience.isnotebooktrusted" - } - ], - "explorer/context": [ - { - "when": "resourceLangId == python && !busyTests && !notebookEditorFocused", - "command": "python.runtests", - "group": "Python" + "python.poetryPath": { + "default": "poetry", + "description": "%python.poetryPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "when": "resourceLangId == python && !busyTests && !notebookEditorFocused", - "command": "python.debugtests", - "group": "Python" + "python.pixiToolPath": { + "default": "pixi", + "description": "%python.pixiToolPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "when": "resourceLangId == python", - "command": "python.execInTerminal", - "group": "Python" + "python.terminal.activateEnvInCurrentTerminal": { + "default": false, + "description": "%python.terminal.activateEnvInCurrentTerminal.description%", + "scope": "resource", + "type": "boolean" }, - { - "when": "resourceLangId == python && python.datascience.featureenabled && !notebookEditorFocused", - "command": "python.datascience.runFileInteractive", - "group": "Python2" + "python.terminal.activateEnvironment": { + "default": true, + "description": "%python.terminal.activateEnvironment.description%", + "scope": "resource", + "type": "boolean" }, - { - "when": "resourceLangId == jupyter", - "command": "python.datascience.opennotebook", - "group": "Python" + "python.terminal.executeInFileDir": { + "default": false, + "description": "%python.terminal.executeInFileDir.description%", + "scope": "resource", + "type": "boolean" }, - { - "when": "resourceLangId == jupyter && python.vscode.channel == 'insiders'", - "command": "python.datascience.opennotebookInPreviewEditor", - "group": "Python" + "python.terminal.focusAfterLaunch": { + "default": false, + "description": "%python.terminal.focusAfterLaunch.description%", + "scope": "resource", + "type": "boolean" }, - { - "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" + "python.terminal.launchArgs": { + "default": [], + "description": "%python.terminal.launchArgs.description%", + "scope": "resource", + "type": "array" }, - { - "command": "python.datascience.exportToHTML", - "title": "%python.command.python.datascience.exportToHTML.title%", - "category": "Python", - "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + "python.terminal.shellIntegration.enabled": { + "default": true, + "markdownDescription": "%python.terminal.shellIntegration.enabled.description%", + "scope": "resource", + "type": "boolean", + "tags": [ + "preview" + ] }, - { - "command": "python.datascience.exportToPDF", - "title": "%python.command.python.datascience.exportToPDF.title%", - "category": "Python", - "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + "python.REPL.enableREPLSmartSend": { + "default": true, + "description": "%python.EnableREPLSmartSend.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.switchOffInsidersChannel", - "title": "%python.command.python.switchOffInsidersChannel.title%", - "category": "Python", - "when": "config.python.insidersChannel != 'default'" + "python.REPL.sendToNativeREPL": { + "default": false, + "description": "%python.REPL.sendToNativeREPL.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.switchToDailyChannel", - "title": "%python.command.python.switchToDailyChannel.title%", - "category": "Python", - "when": "config.python.insidersChannel != 'daily'" + "python.REPL.provideVariables": { + "default": true, + "description": "%python.REPL.provideVariables.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.switchToWeeklyChannel", - "title": "%python.command.python.switchToWeeklyChannel.title%", - "category": "Python", - "when": "config.python.insidersChannel != 'weekly'" + "python.testing.autoTestDiscoverOnSaveEnabled": { + "default": true, + "description": "%python.testing.autoTestDiscoverOnSaveEnabled.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.clearWorkspaceInterpreter", - "title": "%python.command.python.clearWorkspaceInterpreter.title%", - "category": "Python" + "python.testing.autoTestDiscoverOnSavePattern": { + "default": "**/*.py", + "description": "%python.testing.autoTestDiscoverOnSavePattern.description%", + "scope": "resource", + "type": "string" }, - { - "command": "python.resetInterpreterSecurityStorage", - "title": "%python.command.python.resetInterpreterSecurityStorage.title%", - "category": "Python" + "python.testing.cwd": { + "default": null, + "description": "%python.testing.cwd.description%", + "scope": "resource", + "type": "string" }, - { - "command": "python.viewOutput", - "title": "%python.command.python.viewOutput.title%", - "category": "Python" + "python.testing.debugPort": { + "default": 3000, + "description": "%python.testing.debugPort.description%", + "scope": "resource", + "type": "number" }, - { - "command": "python.runTestNode", - "title": "Run", - "category": "Python", - "when": "config.noExists" + "python.testing.promptToConfigure": { + "default": true, + "description": "%python.testing.promptToConfigure.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.discoveringTests", - "category": "Python", - "when": "config.noExists" + "python.testing.pytestArgs": { + "default": [], + "description": "%python.testing.pytestArgs.description%", + "items": { + "type": "string" + }, + "scope": "resource", + "type": "array" }, - { - "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.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.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" - } - }, - "argsExpansion": { - "enum": [ - "none", - "shell" - ], - "description": "Whether to perform shell expansion on \"args\", or pass all values as is.", - "default": "shell" - }, - "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", - "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" - }, - "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", - "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", - "EnableTrustedNotebooks", - "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", - "EnableTrustedNotebooks", - "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.openFilesOnly": { - "type": "boolean", - "default": true, - "description": "Only show errors and warnings for open files rather than for the entire workspace.", - "scope": "resource" - }, - "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.memory.keepLibraryLocalVariables": { - "type": "boolean", - "default": false, - "description": "Allows code analysis to keep library function local variables. Allows code navigation in Python libraries function bodies. Increases memory consumption.", - "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.analysis.cachingLevel": { - "type": "string", - "enum": [ - "Default", - "None", - "System", - "Library" - ], - "default": "Default", - "description": "Defines which types of modules get their analysis cached.", - "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", + "python.testing.pytestEnabled": { "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" + "description": "%python.testing.pytestEnabled.description%", + "scope": "resource", + "type": "boolean" }, - "python.linting.pycodestyleCategorySeverity.E": { - "type": "string", - "default": "Error", - "description": "Severity of pycodestyle message type 'E'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" + "python.testing.pytestPath": { + "default": "pytest", + "description": "%python.testing.pytestPath.description%", + "scope": "machine-overridable", + "type": "string" }, - "python.linting.pycodestyleCategorySeverity.W": { - "type": "string", - "default": "Warning", - "description": "Severity of pycodestyle message type 'W'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" + "python.testing.unittestArgs": { + "default": [ + "-v", + "-s", + ".", + "-p", + "*test*.py" ], - "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": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "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": [], + "description": "%python.testing.unittestArgs.description%", "items": { "type": "string" }, - "scope": "resource" + "scope": "resource", + "type": "array" }, - "python.linting.pylamaEnabled": { - "type": "boolean", + "python.testing.unittestEnabled": { "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" + "description": "%python.testing.unittestEnabled.description%", + "scope": "resource", + "type": "boolean" }, - "python.linting.pylintArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", + "python.venvFolders": { "default": [], + "description": "%python.venvFolders.description%", "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": { + "scope": "machine", "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" + "uniqueItems": true }, - "python.sortImports.path": { - "type": "string", - "description": "Path to isort script, default using inner version", + "python.venvPath": { "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" + "description": "%python.venvPath.description%", + "scope": "machine", + "type": "string" + } + }, + "title": "Python", + "type": "object" + }, + "debuggers": [ + { + "configurationAttributes": { + "attach": { + "properties": { + "connect": { + "label": "Attach by connecting to debugpy over a socket.", + "properties": { + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address to connect to.", + "type": "string" + }, + "port": { + "description": "Port to connect to.", + "type": "number" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "debugAdapterPath": { + "description": "Path (fully qualified) to the python debug adapter executable.", + "type": "string" + }, + "django": { + "default": false, + "description": "Django debugging.", + "type": "boolean" + }, + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address to connect to.", + "type": "string" + }, + "jinja": { + "default": null, + "description": "Jinja template debugging (e.g. Flask).", + "enum": [ + false, + null, + true + ] + }, + "justMyCode": { + "default": true, + "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.", + "type": "boolean" + }, + "listen": { + "label": "Attach by listening for incoming socket connection from debugpy", + "properties": { + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address of the interface to listen on.", + "type": "string" + }, + "port": { + "description": "Port to listen on.", + "type": "number" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" + }, + "pathMappings": { + "default": [], + "items": { + "label": "Path mapping", + "properties": { + "localRoot": { + "default": "${workspaceFolder}", + "label": "Local source root.", + "type": "string" + }, + "remoteRoot": { + "default": "", + "label": "Remote source root.", + "type": "string" + } + }, + "required": [ + "localRoot", + "remoteRoot" + ], + "type": "object" + }, + "label": "Path mappings.", + "type": "array" + }, + "port": { + "description": "Port to connect to.", + "type": "number" + }, + "processId": { + "anyOf": [ + { + "default": "${command:pickProcess}", + "description": "Use process picker to select a process to attach, or Process ID as integer.", + "enum": [ + "${command:pickProcess}" + ] + }, + { + "description": "ID of the local process to attach to.", + "type": "integer" + } + ] + }, + "redirectOutput": { + "default": true, + "description": "Redirect output.", + "type": "boolean" + }, + "showReturnValue": { + "default": true, + "description": "Show return value of functions when stepping.", + "type": "boolean" + }, + "subProcess": { + "default": false, + "description": "Whether to enable Sub Process debugging", + "type": "boolean" + } + } + }, + "launch": { + "properties": { + "args": { + "default": [], + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "type": [ + "array", + "string" + ] + }, + "autoReload": { + "default": {}, + "description": "Configures automatic reload of code on edit.", + "properties": { + "enable": { + "default": false, + "description": "Automatically reload code on edit.", + "type": "boolean" + }, + "exclude": { + "default": [ + "**/.git/**", + "**/.metadata/**", + "**/__pycache__/**", + "**/node_modules/**", + "**/site-packages/**" + ], + "description": "Glob patterns of paths to exclude from auto reload.", + "items": { + "type": "string" + }, + "type": "array" + }, + "include": { + "default": [ + "**/*.py", + "**/*.pyw" + ], + "description": "Glob patterns of paths to include in auto reload.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "console": { + "default": "integratedTerminal", + "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", + "enum": [ + "externalTerminal", + "integratedTerminal", + "internalConsole" + ] + }, + "consoleTitle": { + "default": "Python Debug Console", + "description": "Display name of the debug console or terminal" + }, + "cwd": { + "default": "${workspaceFolder}", + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "type": "string" + }, + "debugAdapterPath": { + "description": "Path (fully qualified) to the python debug adapter executable.", + "type": "string" + }, + "django": { + "default": false, + "description": "Django debugging.", + "type": "boolean" + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "default": {}, + "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.", + "type": "object" + }, + "envFile": { + "default": "${workspaceFolder}/.env", + "description": "Absolute path to a file containing environment variable definitions.", + "type": "string" + }, + "gevent": { + "default": false, + "description": "Enable debugging of gevent monkey-patched code.", + "type": "boolean" + }, + "host": { + "default": "localhost", + "description": "IP address of the of the local debug server (default is localhost).", + "type": "string" + }, + "jinja": { + "default": null, + "description": "Jinja template debugging (e.g. Flask).", + "enum": [ + false, + null, + true + ] + }, + "justMyCode": { + "default": true, + "description": "Debug only user-written code.", + "type": "boolean" + }, + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" + }, + "module": { + "default": "", + "description": "Name of the module to be debugged.", + "type": "string" + }, + "pathMappings": { + "default": [], + "items": { + "label": "Path mapping", + "properties": { + "localRoot": { + "default": "${workspaceFolder}", + "label": "Local source root.", + "type": "string" + }, + "remoteRoot": { + "default": "", + "label": "Remote source root.", + "type": "string" + } + }, + "required": [ + "localRoot", + "remoteRoot" + ], + "type": "object" + }, + "label": "Path mappings.", + "type": "array" + }, + "port": { + "default": 0, + "description": "Debug port (default is 0, resulting in the use of a dynamic port).", + "type": "number" + }, + "program": { + "default": "${file}", + "description": "Absolute path to the program.", + "type": "string" + }, + "purpose": { + "default": [], + "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.", + "items": { + "enum": [ + "debug-test", + "debug-in-terminal" + ], + "enumDescriptions": [ + "Use this configuration while debugging tests using test view or test debug commands.", + "Use this configuration while debugging a file using debug in terminal button in the editor." + ] + }, + "type": "array" + }, + "pyramid": { + "default": false, + "description": "Whether debugging Pyramid applications", + "type": "boolean" + }, + "python": { + "default": "${command:python.interpreterPath}", + "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.", + "type": "string" + }, + "pythonArgs": { + "default": [], + "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".", + "items": { + "type": "string" + }, + "type": "array" + }, + "redirectOutput": { + "default": true, + "description": "Redirect output.", + "type": "boolean" + }, + "showReturnValue": { + "default": true, + "description": "Show return value of functions when stepping.", + "type": "boolean" + }, + "stopOnEntry": { + "default": false, + "description": "Automatically stop after launch.", + "type": "boolean" + }, + "subProcess": { + "default": false, + "description": "Whether to enable Sub Process debugging", + "type": "boolean" + }, + "sudo": { + "default": false, + "description": "Running debug program under elevated permissions (on Unix).", + "type": "boolean" + } + } + } }, - "python.terminal.activateEnvInCurrentTerminal": { - "type": "boolean", - "default": false, - "description": "Activate Python Environment in the current Terminal on load of the Extension.", - "scope": "resource" + "deprecated": "%python.debugger.deprecatedMessage%", + "configurationSnippets": [], + "label": "Python", + "languages": [ + "python" + ], + "type": "python", + "variables": { + "pickProcess": "python.pickLocalProcess" }, - "python.testing.cwd": { - "type": "string", - "default": null, - "description": "Optional working directory for tests.", - "scope": "resource" + "when": "!virtualWorkspace && shellExecutionSupported", + "hiddenWhen": "true" + } + ], + "grammars": [ + { + "language": "pip-requirements", + "path": "./syntaxes/pip-requirements.tmLanguage.json", + "scopeName": "source.pip-requirements" + } + ], + "jsonValidation": [ + { + "fileMatch": ".condarc", + "url": "./schemas/condarc.json" + }, + { + "fileMatch": "environment.yml", + "url": "./schemas/conda-environment.json" + }, + { + "fileMatch": "meta.yaml", + "url": "./schemas/conda-meta.json" + } + ], + "keybindings": [ + { + "command": "python.execSelectionInTerminal", + "key": "shift+enter", + "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !jupyter.ownsSelection && !notebookEditorFocused && !isCompositeNotebook" + }, + { + "command": "python.execInREPL", + "key": "shift+enter", + "when": "config.python.REPL.sendToNativeREPL && editorLangId == python && editorTextFocus && !jupyter.ownsSelection && !notebookEditorFocused && !isCompositeNotebook" + }, + { + "command": "python.execInREPLEnter", + "key": "enter", + "when": "!config.interactiveWindow.executeWithShiftEnter && isCompositeNotebook && activeEditor == 'workbench.editor.repl' && !inlineChatFocused && !notebookCellListFocused" + }, + { + "command": "python.execInInteractiveWindowEnter", + "key": "enter", + "when": "!config.interactiveWindow.executeWithShiftEnter && isCompositeNotebook && activeEditor == 'workbench.editor.interactive' && !inlineChatFocused && !notebookCellListFocused" + } + ], + "languages": [ + { + "aliases": [ + "Jinja" + ], + "extensions": [ + ".j2", + ".jinja2" + ], + "id": "jinja" + }, + { + "aliases": [ + "pip requirements", + "requirements.txt" + ], + "configuration": "./languages/pip-requirements.json", + "filenamePatterns": [ + "**/*requirements*.{txt, in}", + "**/*constraints*.txt", + "**/requirements/*.{txt,in}", + "**/constraints/*.txt" + ], + "filenames": [ + "constraints.txt", + "requirements.in", + "requirements.txt" + ], + "id": "pip-requirements" + }, + { + "filenames": [ + ".condarc" + ], + "id": "yaml" + }, + { + "filenames": [ + ".flake8", + ".pep8", + ".pylintrc", + ".pypirc" + ], + "id": "ini" + }, + { + "filenames": [ + "Pipfile", + "poetry.lock", + "uv.lock" + ], + "id": "toml" + }, + { + "filenames": [ + "Pipfile.lock" + ], + "id": "json" + } + ], + "menus": { + "issue/reporter": [ + { + "command": "python.reportIssue" + } + ], + "testing/item/context": [ + { + "command": "python.copyTestId", + "group": "navigation", + "when": "controllerId == 'python-tests'" + } + ], + "testing/item/gutter": [ + { + "command": "python.copyTestId", + "group": "navigation", + "when": "controllerId == 'python-tests'" + } + ], + "commandPalette": [ + { + "category": "Python", + "command": "python.analysis.restartLanguageServer", + "title": "%python.command.python.analysis.restartLanguageServer.title%", + "when": "!virtualWorkspace && shellExecutionSupported && (editorLangId == python || notebookType == jupyter-notebook)" }, - "python.testing.debugPort": { - "type": "number", - "default": 3000, - "description": "Port number used for debugging of tests.", - "scope": "resource" + { + "category": "Python", + "command": "python.clearCacheAndReload", + "title": "%python.command.python.clearCacheAndReload.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.nosetestArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" + { + "category": "Python", + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.nosetestsEnabled": { - "type": "boolean", - "default": false, - "description": "Enable testing using nosetests.", - "scope": "resource" + { + "category": "Python", + "command": "python.configureTests", + "title": "%python.command.python.configureTests.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "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" + { + "category": "Python", + "command": "python.createEnvironment", + "title": "%python.command.python.createEnvironment.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.promptToConfigure": { - "type": "boolean", - "default": true, - "description": "Prompt to configure a test framework if potential tests directories are discovered.", - "scope": "resource" + { + "category": "Python", + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%", + "when": "false" }, - "python.testing.pytestArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" + { + "category": "Python", + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.pytestEnabled": { - "type": "boolean", - "default": false, - "description": "Enable testing using pytest.", - "scope": "resource" + { + "category": "Python", + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "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" + { + "category": "Python", + "command": "python.execInTerminal-icon", + "icon": "$(play)", + "title": "%python.command.python.execInTerminalIcon.title%", + "when": "false" }, - "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" + { + "category": "Python", + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "false" }, - "python.testing.unittestEnabled": { - "type": "boolean", - "default": false, - "description": "Enable testing using unittest.", - "scope": "resource" + { + "category": "Python", + "command": "python.execSelectionInDjangoShell", + "title": "%python.command.python.execSelectionInDjangoShell.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "python.testing.autoTestDiscoverOnSaveEnabled": { - "type": "boolean", - "default": true, - "description": "Enable auto run test discovery when saving a test file.", - "scope": "resource" + { + "category": "Python", + "command": "python.execSelectionInTerminal", + "title": "%python.command.python.execSelectionInTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "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" - } + { + "category": "Python", + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%", + "when": "false" }, - "python.venvPath": { - "type": "string", - "default": "", - "description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", - "scope": "resource" + { + "category": "Python", + "command": "python.execInREPL", + "title": "%python.command.python.execInREPL.title%", + "when": "false" }, - "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" + { + "category": "Python", + "command": "python.reportIssue", + "title": "%python.command.python.reportIssue.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.workspaceSymbols.enabled": { - "type": "boolean", - "default": false, - "description": "Set to 'true' to enable ctags to provide Workspace Symbols.", - "scope": "resource" + { + "category": "Test", + "command": "testing.reRunFailTests", + "icon": "$(run-errors)", + "title": "%python.command.testing.rerunFailedTests.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "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" + { + "category": "Python", + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "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" + { + "category": "Python", + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.workspaceSymbols.rebuildOnStart": { - "type": "boolean", - "default": true, - "description": "Whether to re-build the tags file on start (defaults to true).", - "scope": "resource" + { + "category": "Python", + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "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" + { + "category": "Python", + "command": "python.viewLanguageServerOutput", + "enablement": "python.hasLanguageServerOutputChannel", + "title": "%python.command.python.viewLanguageServerOutput.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.dataScience.magicCommandsAsComments": { - "type": "boolean", - "default": false, - "description": "Uncomment shell assignments (#!), line magic (#!%) and cell magic (#!%%) when parsing code cells.", - "scope": "resource" + { + "category": "Python", + "command": "python.viewOutput", + "title": "%python.command.python.viewOutput.title%", + "when": "!virtualWorkspace && shellExecutionSupported" + } + ], + "editor/content": [ + { + "group": "Python", + "command": "python.createEnvironment-button", + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" }, - "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" + { + "group": "Python", + "command": "python.createEnvironment-button", + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" + } + ], + "editor/context": [ + { + "submenu": "python.run", + "group": "Python", + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted && !inChat && notebookType != jupyter-notebook" }, - "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" + { + "submenu": "python.runFileInteractive", + "group": "Jupyter2", + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && !isJupyterInstalled && isWorkspaceTrusted && !inChat" + } + ], + "python.runFileInteractive": [ + { + "command": "python.installJupyter", + "group": "Jupyter2", + "when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported" + } + ], + "python.run": [ + { + "command": "python.execInTerminal", + "group": "Python", + "when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported" }, - "python.dataScience.debugJustMyCode": { - "type": "boolean", - "default": true, - "description": "When debugging, debug just my code.", - "scope": "resource" + { + "command": "python.execSelectionInDjangoShell", + "group": "Python", + "when": "editorHasSelection && editorLangId == python && python.isDjangoProject && !virtualWorkspace && shellExecutionSupported" }, - "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" + { + "command": "python.execSelectionInTerminal", + "group": "Python", + "when": "!config.python.REPL.sendToNativeREPL && editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported" }, - "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" + { + "command": "python.execInREPL", + "group": "Python", + "when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported && config.python.REPL.sendToNativeREPL" + } + ], + "editor/title/run": [ + { + "command": "python.execInTerminal-icon", + "group": "navigation@0", + "title": "%python.command.python.execInTerminalIcon.title%", + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, - "python.showStartPage": { - "type": "boolean", - "default": true, - "description": "Show the Python Start Page when a new update is released.", - "scope": "application" + { + "command": "python.execInDedicatedTerminal", + "group": "navigation@0", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" } - } + ], + "explorer/context": [ + { + "command": "python.execInTerminal", + "group": "Python", + "when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported" + } + ], + "file/newFile": [ + { + "command": "python.createNewFile", + "group": "file", + "when": "!virtualWorkspace" + } + ], + "view/title": [ + { + "command": "testing.reRunFailTests", + "when": "view == workbench.view.testing && hasFailedTests && !virtualWorkspace && shellExecutionSupported", + "group": "navigation@1" + } + ] }, - "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" - ] - }, + "submenus": [ { - "id": "jinja", - "extensions": [ - ".jinja2", - ".j2" - ], - "aliases": [ - "Jinja" - ] + "id": "python.run", + "label": "%python.editor.context.submenu.runPython%", + "icon": "$(play)" }, { - "id": "jupyter", - "aliases": [ - "Jupyter", - "Notebook" - ], - "extensions": [ - ".ipynb" - ] + "id": "python.runFileInteractive", + "label": "%python.editor.context.submenu.runPythonInteractive%" } ], - "grammars": [ + "viewsWelcome": [ { - "language": "pip-requirements", - "scopeName": "source.pip-requirements", - "path": "./syntaxes/pip-requirements.tmLanguage.json" + "view": "testing", + "contents": "Configure a test framework to see your tests here.\n[Configure Python Tests](command:python.configureTests)", + "when": "!virtualWorkspace && shellExecutionSupported" } ], - "jsonValidation": [ + "yamlValidation": [ { "fileMatch": ".condarc", "url": "./schemas/condarc.json" @@ -3284,405 +1504,310 @@ "url": "./schemas/conda-meta.json" } ], - "yamlValidation": [ - { - "fileMatch": ".condarc", - "url": "./schemas/condarc.json" + "languageModelTools": [ + { + "name": "get_python_environment_details", + "displayName": "Get Python Environment Info", + "userDescription": "%python.languageModelTools.get_python_environment_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Python Environment (conda, venv, etc), 2. Version of Python, 3. List of all installed Python packages with their versions. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "getPythonEnvironmentInfo", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(snake)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace to get the environment information for." + } + }, + "required": [] + } }, { - "fileMatch": "environment.yml", - "url": "./schemas/conda-environment.json" + "name": "get_python_executable_details", + "displayName": "Get Python Executable", + "userDescription": "%python.languageModelTools.get_python_executable_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "getPythonExecutableCommand", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(terminal)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." + } + }, + "required": [] + } }, { - "fileMatch": "meta.yaml", - "url": "./schemas/conda-meta.json" - } - ], - "views": { - "test": [ - { - "id": "python_tests", - "name": "Python", - "when": "testsDiscovered" + "name": "install_python_packages", + "displayName": "Install Python Package", + "userDescription": "%python.languageModelTools.install_python_packages.userDescription%", + "modelDescription": "Installs Python packages in the given workspace. Use this tool to install Python packages in the user's chosen Python environment. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool should only be used to install Python packages using package managers like pip or conda (works with any Python environment: venv, virtualenv, pipenv, poetry, pyenv, pixi, conda, etc.). Do not use this tool to install npm packages, system packages (apt/brew/yum), Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "installPythonPackage", + "tags": [ + "python", + "python environment", + "install python package", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(package)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "packageList": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of Python packages to install." + }, + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace into which the packages are installed. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." + } + }, + "required": [ + "packageList" + ] } - ] - }, - "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" - ] - } - ], - "notebookProvider": [ + }, { - "viewType": "jupyter-notebook", - "displayName": "Jupyter Notebook (preview)", - "selector": [ - { - "filenamePattern": "*.ipynb" - } + "name": "configure_python_environment", + "displayName": "Configure Python Environment", + "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools or running any Python command in the terminal. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "userDescription": "%python.languageModelTools.configure_python_environment.userDescription%", + "toolReferenceName": "configurePythonEnvironment", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool" ], - "priority": "option" + "icon": "$(gear)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + } + }, + { + "name": "create_virtual_environment", + "displayName": "Create a Virtual Environment", + "modelDescription": "This tool will create a Virual Environment", + "tags": [], + "canBeReferencedInPrompt": false, + "inputSchema": { + "type": "object", + "properties": { + "packageList": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of packages to install." + }, + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + }, + "when": "false" + }, + { + "name": "selectEnvironment", + "displayName": "Select a Python Environment", + "modelDescription": "This tool will prompt the user to select an existing Python Environment", + "tags": [], + "canBeReferencedInPrompt": false, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + }, + "when": "false" } ] }, + "copilot": { + "tests": { + "getSetupConfirmation": "python.copilotSetupTests" + } + }, "scripts": { "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", + "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", + "compileApi": "node ./node_modules/typescript/lib/tsc.js -b ./pythonExtensionApi/tsconfig.json", "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:unittests": "mocha --config ./build/.mocha.unittests.json", + "test:unittests:cover": "nyc --no-clean --nycrc-path ./build/.nycrc mocha --config ./build/.mocha.unittests.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:cover:report": "nyc --nycrc-path build/.nycrc report --reporter=text --reporter=html --reporter=text-summary --reporter=cobertura", + "test:functional:cover": "nyc --no-clean --nycrc-path ./build/.nycrc mocha --require source-map-support/register --config ./build/.mocha.functional.json", + "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", + "testDebugger:cover": "nyc --no-clean --use-spawn-wrap --nycrc-path ./build/.nycrc --require source-map-support/register node ./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", + "testSingleWorkspace:cover": "nyc --no-clean --use-spawn-wrap --nycrc-path ./build/.nycrc --require source-map-support/register node ./out/test/standardTest.js", + "preTestJediLSP": "node ./out/test/languageServers/jedi/lspSetup.js", + "testJediLSP": "node ./out/test/languageServers/jedi/lspSetup.js && cross-env CODE_TESTS_WORKSPACE=src/test VSC_PYTHON_CI_TEST_GREP='Language Server:' node ./out/test/testBootstrap.js ./out/test/standardTest.js && node ./out/test/languageServers/jedi/lspTeardown.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", + "testSmoke": "cross-env INSTALL_JUPYTER_EXTENSION=true \"node ./out/test/smokeTest.js\"", + "testInsiders": "cross-env VSC_PYTHON_CI_TEST_VSC_CHANNEL=insiders INSTALL_PYLANCE_EXTENSION=true TEST_FILES_SUFFIX=insiders.test CODE_TESTS_WORKSPACE=src/testMultiRootWkspc/smokeTests \"node ./out/test/standardTest.js\"", "lint-staged": "node gulpfile.js", - "lint": "tslint src/**/*.ts -t verbose", - "prettier-fix": "prettier 'src/**/*.ts*' --write && prettier 'build/**/*.js' --write", + "lint": "eslint src build pythonExtensionApi", + "lint-fix": "eslint --fix src build pythonExtensionApi gulpfile.js", + "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", + "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", + "check-python": "npm run check-python:ruff && npm run check-python:pyright", + "check-python:ruff": "cd python_files && python -m pip install -U ruff && python -m ruff check . && python -m ruff format --check", + "check-python:pyright": "cd python_files && npx --yes pyright@1.1.308 .", "clean": "gulp clean", + "addExtensionPackDependencies": "gulp addExtensionPackDependencies", "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", + "@iarna/toml": "^3.0.0", + "@vscode/extension-telemetry": "^0.8.4", "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", + "fs-extra": "^11.2.0", + "glob": "^7.2.0", + "iconv-lite": "^0.6.3", + "inversify": "^6.0.2", + "jsonc-parser": "^3.0.0", + "lodash": "^4.18.1", + "minimatch": "^5.1.8", "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", + "reflect-metadata": "^0.2.2", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "sanitize-filename": "^1.6.3", - "semver": "^5.5.0", + "semver": "^7.5.2", "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", + "sudo-prompt": "^9.2.1", + "tmp": "^0.2.5", + "uint64be": "^3.0.0", + "unicode": "^14.0.0", "vscode-debugprotocol": "^1.28.0", - "vscode-extension-telemetry": "0.1.4", - "vscode-jsonrpc": "^6.0.0-next.3", - "vscode-languageclient": "^7.0.0-next.6", - "vscode-languageserver": "^7.0.0-next.4", - "vscode-languageserver-protocol": "^3.16.0-next.5", - "vscode-tas-client": "^0.0.864", - "vsls": "^0.3.1291", + "vscode-jsonrpc": "^9.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.12", + "vscode-languageserver-protocol": "^3.17.6-next.10", + "vscode-tas-client": "^0.1.84", + "which": "^2.0.2", "winreg": "^1.2.4", - "winston": "^3.2.1", - "ws": "^6.0.0", - "xml2js": "^0.4.19", - "zeromq": "^6.0.0-beta.6" + "xml2js": "^0.5.0" }, "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", + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/bent": "^7.3.0", "@types/chai": "^4.1.2", - "@types/chai-arrays": "^1.0.2", + "@types/chai-arrays": "^2.0.0", "@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/download": "^8.0.1", + "@types/fs-extra": "^11.0.4", + "@types/glob": "^7.2.0", "@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/mocha": "^9.1.0", + "@types/node": "^22.19.1", "@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/sinon": "^17.0.3", "@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/webpack-bundle-analyzer": "^2.13.0", + "@types/tmp": "^0.0.33", + "@types/vscode": "^1.95.0", + "@types/which": "^2.0.1", "@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", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vscode/test-electron": "^2.3.8", + "@vscode/vsce": "^2.27.0", + "bent": "^7.3.12", "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", + "copy-webpack-plugin": "^9.1.0", + "cross-env": "^7.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", + "del": "^6.0.0", + "download": "^8.0.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-no-only-tests": "^3.3.0", "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.0.1", - "mocha-junit-reporter": "^1.17.0", + "expose-loader": "^3.1.0", + "flat": "^5.0.2", + "get-port": "^5.1.1", + "gulp": "^5.0.0", + "gulp-typescript": "^5.0.0", + "mocha": "^11.1.0", + "mocha-junit-reporter": "^2.0.2", "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", + "node-loader": "^1.0.2", + "node-polyfill-webpack-plugin": "^1.1.4", "nyc": "^15.0.0", - "playwright-chromium": "^0.13.0", - "plotly.js-dist": "^1.54.5", - "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": "^2.1.2", "shortid": "^2.2.8", - "sinon": "^8.0.1", - "slickgrid": "^2.4.17", - "socket.io": "^2.3.0", + "sinon": "^18.0.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": "^2.3.2", - "thread-loader": "^2.1.3", - "transform-loader": "^0.2.4", - "ts-loader": "^5.3.0", + "ts-loader": "^9.2.8", "ts-mockito": "^2.5.0", - "ts-node": "^8.3.0", + "ts-node": "^10.7.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": "^3.9.7", - "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", + "typescript": "~5.2", + "uuid": "^8.3.2", + "webpack": "^5.105.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^4.9.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", + "webpack-merge": "^5.8.0", + "webpack-node-externals": "^3.0.0", + "webpack-require-from": "^1.8.6", + "worker-loader": "^3.0.8", "yargs": "^15.3.1" - }, - "__metadata": { - "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5", - "publisherDisplayName": "Microsoft", - "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } -} \ No newline at end of file +} diff --git a/package.nls.de.json b/package.nls.de.json deleted file mode 100644 index 77ec14b3ee9c..000000000000 --- a/package.nls.de.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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 deleted file mode 100644 index 227e268dbc8d..000000000000 --- a/package.nls.es.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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 deleted file mode 100644 index ad28a94d6e09..000000000000 --- a/package.nls.fr.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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 deleted file mode 100644 index c16d6ce74241..000000000000 --- a/package.nls.it.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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 deleted file mode 100644 index a2db0a823636..000000000000 --- a/package.nls.ja.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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 index 81fa8210f937..57f2ed95b2c0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,593 +1,177 @@ { - "python.command.python.sortImports.title": "Sort Imports", - "python.command.python.startREPL.title": "Start REPL", + "python.command.python.startTerminalREPL.title": "Start Terminal REPL", + "python.languageModelTools.get_python_environment_details.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", + "python.languageModelTools.install_python_packages.userDescription": "Installs Python packages in a Python Environment.", + "python.languageModelTools.get_python_executable_details.userDescription": "Get executable info for a Python Environment", + "python.languageModelTools.configure_python_environment.userDescription": "Configure a Python Environment for a workspace", + "python.command.python.startNativeREPL.title": "Start Native Python REPL", + "python.command.python.createEnvironment.title": "Create Environment...", + "python.command.python.createNewFile.title": "New Python File", "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.execInTerminalIcon.title": "Run Python File", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated 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.installJupyter.title": "Install the Jupyter extension", "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.testing.rerunFailedTests.title": "Rerun Failed Tests", "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", + "python.command.python.execSelectionInTerminal.shortTitle": "Run Selection/Line", + "python.command.python.execInREPL.title": "Run Selection/Line in Native Python REPL", "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.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.reportIssue.title": "Report Issue...", + "python.command.python.clearCacheAndReload.title": "Clear Cache and Reload Window", "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.", - "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.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/gathersurvey\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" + "python.command.python.launchTensorBoard.title": "Launch TensorBoard", + "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", + "python.command.python.testing.copyTestId.title": "Copy Test Id", + "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", + "python.createEnvironment.trigger.description": "Detect if environment creation is required for the current project", + "python.menu.createNewFile.title": "Python File", + "python.editor.context.submenu.runPython": "Run Python", + "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", + "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", + "python.autoComplete.extraPaths.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.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", + "python.debugger.deprecatedMessage": "This configuration will be deprecated soon. Please replace `python` with `debugpy` to use the new Python Debugger extension.", + "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", + "python.envFile.description": "Absolute path to a file containing environment variable definitions.", + "python.useEnvironmentsExtension.description": "Enables the Python Environments extension. Requires window reload on change.", + "python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.", + "python.experiments.optInto.description": "List of experiments to opt into. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", + "python.experiments.optOutFrom.description": "List of experiments to opt out of. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", + "python.experiments.All.description": "Combined list of all experiments.", + "python.experiments.pythonSurveyNotification.description": "Denotes the Python Survey Notification experiment.", + "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", + "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", + "python.experiments.pythonDiscoveryUsingWorkers.description": "Enables use of worker threads to do heavy computation when discovering interpreters.", + "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", + "python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.", + "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", + "python.languageServer.description": "Defines type of the language server.", + "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", + "python.languageServer.jediDescription": "Use Jedi behind the Language Server Protocol (LSP) as a language server.", + "python.languageServer.pylanceDescription": "Use Pylance as a language server.", + "python.languageServer.noneDescription": "Disable language server capabilities.", + "python.interpreter.infoVisibility.description": "Controls when to display information of selected interpreter in the status bar.", + "python.interpreter.infoVisibility.never.description": "Never display information.", + "python.interpreter.infoVisibility.onPythonRelated.description": "Only display information if Python-related files are opened.", + "python.interpreter.infoVisibility.always.description": "Always display information.", + "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", + "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", + "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", + "python.locator.description": "[Experimental] Select implementation of environment locators. This is an experimental setting while we test native environment location.", + "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", + "python.poetryPath.description": "Path to the poetry executable.", + "python.pixiToolPath.description": "Path to the pixi executable.", + "python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.", + "python.REPL.sendToNativeREPL.description": "Toggle to send code to Python REPL instead of the terminal on execution. Turning this on will change the behavior for both Smart Send and Run Selection/Line in the Context Menu.", + "python.REPL.provideVariables.description": "Toggle to provide variables for the REPL variable view for the native REPL.", + "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", + "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", + "python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.", + "python.terminal.shellIntegration.enabled.description": "Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) for the terminals running python. Shell integration enhances the terminal experience by enabling command decorations, run recent command, improving accessibility among other things. Note: PyREPL (available in Python 3.13+) is automatically disabled when shell integration is enabled to avoid cursor indentation issues.", + "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", + "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", + "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", + "python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.", + "python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.", + "python.testing.autoTestDiscoverOnSaveEnabled.description": "Enable auto run test discovery when saving a test file.", + "python.testing.autoTestDiscoverOnSavePattern.description": "Glob pattern used to determine which files are used by autoTestDiscoverOnSaveEnabled.", + "python.testing.cwd.description": "Optional working directory for tests.", + "python.testing.debugPort.description": "Port number used for debugging of tests.", + "python.testing.promptToConfigure.description": "Prompt to configure a test framework if potential tests directories are discovered.", + "python.testing.pytestArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.testing.pytestEnabled.description": "Enable testing using pytest.", + "python.testing.pytestPath.description": "Path to pytest. You can use a custom version of pytest by modifying this setting to include the full path.", + "python.testing.unittestArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.testing.unittestEnabled.description": "Enable testing using unittest.", + "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", + "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", + "walkthrough.pythonWelcome.title": "Get Started with Python Development", + "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", + "walkthrough.step.python.createPythonFile.title": "Create a Python file", + "walkthrough.step.python.createPythonFolder.title": "Open a Python project folder", + "walkthrough.step.python.createPythonFile.description": { + "message": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "comment": [ + "{Locked='](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.createPythonFolder.description": { + "message": "[Open](command:workbench.action.files.openFolder) or create a project folder.\n[Open Project Folder](command:workbench.action.files.openFolder)", + "comment": [ + "{Locked='](command:workbench.action.files.openFolder'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.installPythonWin8.title": "Install Python", + "walkthrough.step.python.installPythonWin8.description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", + "walkthrough.step.python.installPythonMac.title": "Install Python", + "walkthrough.step.python.installPythonMac.description": { + "message": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", + "comment": [ + "{Locked='](command:python.installPythonOnMac'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.installPythonLinux.title": "Install Python", + "walkthrough.step.python.installPythonLinux.description": { + "message": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", + "comment": [ + "{Locked='](command:python.installPythonOnLinux'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.selectInterpreter.title": "Select a Python Interpreter", + "walkthrough.step.python.createEnvironment.title": "Select or create a Python environment", + "walkthrough.step.python.createEnvironment.description": { + "message": "Create an environment for your Python project or use [Select Python Interpreter](command:python.setInterpreter) to select an existing one.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).", + "comment": [ + "{Locked='](command:python.createEnvironment'}", + "{Locked='](command:workbench.action.showCommands'}", + "{Locked='](command:python.setInterpreter'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.runAndDebug.title": "Run and debug your Python file", + "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", + "walkthrough.step.python.learnMoreWithDS.title": "Keep exploring!", + "walkthrough.step.python.learnMoreWithDS.description": { + "message": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", + "comment": [ + "{Locked='](command:workbench.action.showCommands'}", + "{Locked='](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.pythonDataScienceWelcome.title": "Get Started with Python for Data Science", + "walkthrough.pythonDataScienceWelcome.description": "Your first steps to getting started with a Data Science project with Python!", + "walkthrough.step.python.installJupyterExt.title": "Install Jupyter extension", + "walkthrough.step.python.installJupyterExt.description": "If you haven't already, install the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") to take full advantage of notebooks experiences in VS Code!\n \n[Search Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\")", + "walkthrough.step.python.createNewNotebook.title": "Create or open a Jupyter Notebook", + "walkthrough.step.python.createNewNotebook.description": "Right click in the file explorer and create a new file with an .ipynb extension. Or, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create New Blank Notebook``.\n[Create new Jupyter Notebook](command:toSide:jupyter.createnewnotebook)\n If you have an existing project, you can also [open a folder](command:workbench.action.files.openFolder) and/or clone a project from GitHub: [clone a Git repository](command:git.clone).", + "walkthrough.step.python.openInteractiveWindow.title": "Open the Python Interactive Window", + "walkthrough.step.python.openInteractiveWindow.description": "The Python Interactive Window is a Python shell where you can execute and view the results of your Python code. You can create cells on a Python file by typing ``#%%``.\n \nTo open the interactive window anytime, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create Interactive Window``.\n[Open Interactive Window](command:jupyter.createnewinteractive)", + "walkthrough.step.python.dataScienceLearnMore.title": "Find out more!", + "walkthrough.step.python.dataScienceLearnMore.description": "📒 Take a look into the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") features, by looking for \"Jupyter\" in the [Command Palette](command:workbench.action.showCommands). \n 🏃🏻 Find out more features in our [Tutorials](https://aka.ms/AAdjzpd). \n[Learn more](https://aka.ms/AAdar6q)", + "walkthrough.step.python.createPythonFile.altText": "Open a Python file or a folder with a Python project.", + "walkthrough.step.python.selectInterpreter.altText": "Selecting a Python interpreter from the status bar", + "walkthrough.step.python.createEnvironment.altText": "Creating a Python environment from the Command Palette", + "walkthrough.step.python.runAndDebug.altText": "How to run and debug in VS Code with F5 or the play button on the top right.", + "walkthrough.step.python.learnMoreWithDS.altText": "Image representing our documentation page and mailing list resources.", + "walkthrough.step.python.installJupyterExt.altText": "Creating a new Jupyter notebook", + "walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook", + "walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window", + "walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources." } diff --git a/package.nls.ko-kr.json b/package.nls.ko-kr.json deleted file mode 100644 index 5309a9b07b81..000000000000 --- a/package.nls.ko-kr.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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 deleted file mode 100644 index ba4968e78535..000000000000 --- a/package.nls.nl.json +++ /dev/null @@ -1,153 +0,0 @@ -{ - "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 deleted file mode 100644 index 0e6c5a0ec3d7..000000000000 --- a/package.nls.pl.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "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 deleted file mode 100644 index 1acc94053ca0..000000000000 --- a/package.nls.pt-br.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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 deleted file mode 100644 index 9b6b0a5661da..000000000000 --- a/package.nls.ru.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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 deleted file mode 100644 index 0e648bb38fdf..000000000000 --- a/package.nls.tr.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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 deleted file mode 100644 index aaf5460a3c55..000000000000 --- a/package.nls.zh-cn.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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 deleted file mode 100644 index 96eb10983a9a..000000000000 --- a/package.nls.zh-tw.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "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 deleted file mode 100644 index 45bddaa1b591..000000000000 --- a/pvsc.code-workspace +++ /dev/null @@ -1,53 +0,0 @@ -{ - "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/pythonExtensionApi/.eslintrc b/pythonExtensionApi/.eslintrc new file mode 100644 index 000000000000..8828c49002ed --- /dev/null +++ b/pythonExtensionApi/.eslintrc @@ -0,0 +1,11 @@ +{ + "overrides": [ + { + "files": ["**/main.d.ts"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "padding-line-between-statements": ["error", { "blankLine": "always", "prev": "export", "next": "*" }] + } + } + ] +} diff --git a/pythonExtensionApi/.npmignore b/pythonExtensionApi/.npmignore new file mode 100644 index 000000000000..283d589ea5fe --- /dev/null +++ b/pythonExtensionApi/.npmignore @@ -0,0 +1,8 @@ +example/** +dist/ +out/**/*.map +out/**/*.tsbuildInfo +src/ +.eslintrc* +.eslintignore +tsconfig*.json diff --git a/pythonExtensionApi/LICENSE.md b/pythonExtensionApi/LICENSE.md new file mode 100644 index 000000000000..767f4076ba05 --- /dev/null +++ b/pythonExtensionApi/LICENSE.md @@ -0,0 +1,21 @@ +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. diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md new file mode 100644 index 000000000000..5208d90cdfa5 --- /dev/null +++ b/pythonExtensionApi/README.md @@ -0,0 +1,55 @@ +# Python extension's API + +This npm module implements an API facade for the Python extension in VS Code. + +## Example + +First we need to define a `package.json` for the extension that wants to use the API: + +```jsonc +{ + "name": "...", + ... + // depend on the Python extension + "extensionDependencies": [ + "ms-python.python" + ], + // Depend on the Python extension facade npm module to get easier API access to the + // core extension. + "dependencies": { + "@vscode/python-extension": "...", + "@types/vscode": "..." + }, +} +``` + +Update `"@types/vscode"` to [a recent version](https://code.visualstudio.com/updates/) of VS Code, say `"^1.81.0"` for VS Code version `"1.81"`, in case there are any conflicts. + +The actual source code to get the active environment to run some script could look like this: + +```typescript +// Import the API +import { PythonExtension } from '@vscode/python-extension'; + +... + +// Load the Python extension API +const pythonApi: PythonExtension = await PythonExtension.api(); + +// This will return something like /usr/bin/python +const environmentPath = pythonApi.environments.getActiveEnvironmentPath(); + +// `environmentPath.path` carries the value of the setting. Note that this path may point to a folder and not the +// python binary. Depends entirely on how the env was created. +// E.g., `conda create -n myenv python` ensures the env has a python binary +// `conda create -n myenv` does not include a python binary. +// Also, the path specified may not be valid, use the following to get complete details for this environment if +// need be. + +const environment = await pythonApi.environments.resolveEnvironment(environmentPath); +if (environment) { + // run your script here. +} +``` + +Check out [the wiki](https://aka.ms/pythonEnvironmentApi) for many more examples and usage. diff --git a/pythonExtensionApi/SECURITY.md b/pythonExtensionApi/SECURITY.md new file mode 100644 index 000000000000..a050f362c152 --- /dev/null +++ b/pythonExtensionApi/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 definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), 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 [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/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json new file mode 100644 index 000000000000..e462fc1c888a --- /dev/null +++ b/pythonExtensionApi/package-lock.json @@ -0,0 +1,157 @@ +{ + "name": "@vscode/python-extension", + "version": "1.0.6", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@vscode/python-extension", + "version": "1.0.6", + "license": "MIT", + "devDependencies": { + "@types/vscode": "^1.93.0", + "source-map": "^0.8.0-beta.0", + "typescript": "~5.2" + }, + "engines": { + "node": ">=22.17.0", + "vscode": "^1.93.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz", + "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/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 + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + }, + "dependencies": { + "@types/vscode": { + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz", + "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "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 + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } +} diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json new file mode 100644 index 000000000000..11e0445aa8da --- /dev/null +++ b/pythonExtensionApi/package.json @@ -0,0 +1,43 @@ +{ + "name": "@vscode/python-extension", + "description": "An API facade for the Python extension in VS Code", + "version": "1.0.6", + "author": { + "name": "Microsoft Corporation" + }, + "keywords": [ + "Python", + "VSCode", + "API" + ], + "main": "./out/main.js", + "types": "./out/main.d.ts", + "engines": { + "node": ">=22.21.1", + "vscode": "^1.93.0" + }, + "license": "MIT", + "homepage": "https://github.com/microsoft/vscode-python/tree/main/pythonExtensionApi", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "devDependencies": { + "typescript": "~5.2", + "@types/vscode": "^1.102.0", + "source-map": "^0.8.0-beta.0" + }, + "scripts": { + "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", + "prepack": "npm run all:publish", + "compile": "node ./node_modules/typescript/lib/tsc.js -b ./tsconfig.json", + "clean": "node ../node_modules/rimraf/bin.js out", + "lint": "node ../node_modules/eslint/bin/eslint.js --ext ts src", + "all": "npm run clean && npm run compile", + "formatTypings": "node ../node_modules/eslint/bin/eslint.js --fix ./out/main.d.ts", + "all:publish": "git clean -xfd . && npm install && npm run compile && npm run formatTypings" + } +} diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts new file mode 100644 index 000000000000..2173245cbb28 --- /dev/null +++ b/pythonExtensionApi/src/main.ts @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ +export interface PythonExtension { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + */ + ready: Promise; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/python_files/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + + /** + * Gets the path to the debugger package used by the extension. + */ + getDebuggerPackagePath(): Promise; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Resource the environment changed for. + */ + readonly resource: Resource | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/pythonExtensionApi/tsconfig.json b/pythonExtensionApi/tsconfig.json new file mode 100644 index 000000000000..9ab7617023df --- /dev/null +++ b/pythonExtensionApi/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, + "module": "commonjs", + "target": "es2018", + "outDir": "./out", + "lib": [ + "es6", + "es2018", + "dom", + "ES2019", + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "declaration": true + }, + "exclude": [ + "node_modules", + "out" + ] +} diff --git a/pythonFiles/Notebooks intro.ipynb b/pythonFiles/Notebooks intro.ipynb deleted file mode 100644 index 4fcd31fad674..000000000000 --- a/pythonFiles/Notebooks intro.ipynb +++ /dev/null @@ -1,203 +0,0 @@ -{ - "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/completion.py b/pythonFiles/completion.py deleted file mode 100644 index 7a468106a2f3..000000000000 --- a/pythonFiles/completion.py +++ /dev/null @@ -1,696 +0,0 @@ -import os -import os.path -import io -import re -import sys -import json -import traceback -import platform - -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", - } - - def __init__(self): - self.default_sys_path = sys.path - 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): - # 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 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): - """Recursively walk through directories looking for top level module. - - Jedi will use current filepath to look for another modules at same - path, but it will not be able to see modules **above**, so our goal - 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")): - return cls._get_top_level_module(_path) - return path - - def _generate_signature(self, completion): - """Generate signature with function arguments. - """ - if completion.type in ["module"] or not hasattr(completion, "params"): - return "" - return "%s(%s)" % ( - completion.name, - ", ".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. - - Returns: - Tuple with original signature object, name and value. - """ - _signatures = [] - try: - 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 - - name = self._get_param_name(param) - if param.name == "self" and pos == 0: - continue - if name.startswith("*"): - continue - - value = self._get_param_value(param) - _signatures.append((signature, name, value)) - return _signatures - - 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: - script: Instance of jedi.api.Script object. - identifier: Unique completion identifier to pass back to VSCode. - prefix: String with prefix to filter function arguments. - Used only when fuzzy matcher turned off. - - Returns: - Serialized string to send to VSCode. - """ - _completions = [] - - for signature, name, value in self._get_call_signatures(script): - if not self.fuzzy_matcher and not name.lower().startswith(prefix.lower()): - continue - _completion = { - "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=" % (name) - else: - _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: - 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}) - - 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. - - Args: - script: Instance of jedi.api.Script object. - identifier: Unique completion identifier to pass back to VSCode. - - Returns: - Serialized string to send to VSCode. - """ - 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: - return d - return definition - - 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: - 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.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 = "" - - 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 = 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), - "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}) - - 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}) - - 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 _set_request_config(self, config): - """Sets config values for current request. - - This includes sys.path modifications which is getting restored to - default value on each request so each project should be isolated - from each other. - - Args: - 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) - jedi.settings.case_insensitive_completion = config.get( - "caseInsensitiveCompletion", True - ) - for path in config.get("extraPaths", []): - if path and path not in sys.path: - sys.path.insert(0, path) - - 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._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") - - if lookup == "names": - return self._serialize_definitions( - jedi.api.names( - 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._serialize_completions( - script, request["id"], request.get("prefix", "") - ) - - def _write_response(self, response): - sys.stdout.write(response + "\n") - sys.stdout.flush() - - def watch(self): - while True: - try: - 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.flush() - - -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/install_debugpy.py b/pythonFiles/install_debugpy.py deleted file mode 100644 index cb21efd91927..000000000000 --- a/pythonFiles/install_debugpy.py +++ /dev/null @@ -1,63 +0,0 @@ -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/normalizeForInterpreter.py b/pythonFiles/normalizeForInterpreter.py deleted file mode 100644 index 34b31d56b398..000000000000 --- a/pythonFiles/normalizeForInterpreter.py +++ /dev/null @@ -1,154 +0,0 @@ -# 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/printEnvVariablesToFile.py b/pythonFiles/printEnvVariablesToFile.py deleted file mode 100644 index be966bcac28c..000000000000 --- a/pythonFiles/printEnvVariablesToFile.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 deleted file mode 100644 index 52c7c96d11e7..000000000000 --- a/pythonFiles/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[tool.black] -exclude = ''' - -( - /( - .data - | .vscode - | lib - )/ -) -''' diff --git a/pythonFiles/pyvsc-run-isolated.py b/pythonFiles/pyvsc-run-isolated.py deleted file mode 100644 index 1f6770787490..000000000000 --- a/pythonFiles/pyvsc-run-isolated.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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 deleted file mode 100644 index b62802ce2c53..000000000000 --- a/pythonFiles/refactor.py +++ /dev/null @@ -1,397 +0,0 @@ -# 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/sortImports.py b/pythonFiles/sortImports.py deleted file mode 100644 index 07381b915f4c..000000000000 --- a/pythonFiles/sortImports.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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) - -# Work around stdin buffering issues on windows (https://bugs.python.org/issue40540) -# caused in part by isort seeking within the stdin stream by replacing the -# stream with something which is definitely seekable. -try: - # python 3 - stdin = sys.stdin.buffer -except AttributeError: - # python 2 - stdin = sys.stdin - -sys.stdin = io.BytesIO(stdin.read()) -# End workaround - -import isort.main - -isort.main.main() diff --git a/pythonFiles/symbolProvider.py b/pythonFiles/symbolProvider.py deleted file mode 100644 index 033ce4b99900..000000000000 --- a/pythonFiles/symbolProvider.py +++ /dev/null @@ -1,89 +0,0 @@ -# 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/adapter/__main__.py b/pythonFiles/testing_tools/adapter/__main__.py deleted file mode 100644 index 2f17c6e50a61..000000000000 --- a/pythonFiles/testing_tools/adapter/__main__.py +++ /dev/null @@ -1,106 +0,0 @@ -# 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: - seppos = argv.index("--") - toolargs = argv[seppos + 1 :] - argv = argv[:seppos] - 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 deleted file mode 100644 index 798aea1e93f1..000000000000 --- a/pythonFiles/testing_tools/adapter/discovery.py +++ /dev/null @@ -1,117 +0,0 @@ -# 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 deleted file mode 100644 index 3e6ae5189cb8..000000000000 --- a/pythonFiles/testing_tools/adapter/errors.py +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index f4ab602d8f18..000000000000 --- a/pythonFiles/testing_tools/adapter/info.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from collections import namedtuple - - -class TestPath(namedtuple("TestPath", "root relfile func sub")): - """Where to find a single test.""" - - def __new__(cls, root, relfile, func, sub=None): - self = super(TestPath, 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 TestInfo(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(TestInfo, 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 deleted file mode 100644 index e894f7bcdb8e..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 3d3eec09a199..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_cli.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 deleted file mode 100644 index 51c94527302d..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_discovery.py +++ /dev/null @@ -1,109 +0,0 @@ -# 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 deleted file mode 100644 index 87e439e30c7e..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py +++ /dev/null @@ -1,604 +0,0 @@ -# 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 TestInfo, TestPath -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 = TestInfo( - id=nodeid, - name=item.name, - path=TestPath( - 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 deleted file mode 100644 index bacdef7b9a00..000000000000 --- a/pythonFiles/testing_tools/adapter/report.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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 deleted file mode 100644 index 1e650522f64c..000000000000 --- a/pythonFiles/testing_tools/adapter/util.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import contextlib - -try: - from io import StringIO -except ImportError: - from StringIO import StringIO # 2.7 -import os.path -import sys - - -@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 hide_stdio(): - """Swallow stdout and stderr.""" - ignored = StdioStream() - sys.stdout = ignored - sys.stderr = ignored - try: - yield ignored - finally: - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - - -if sys.version_info < (3,): - - class StdioStream(StringIO): - def write(self, msg): - StringIO.write(self, msg.decode()) - - -else: - StdioStream = StringIO - - -############################# -# 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 deleted file mode 100644 index 1eeef194f8f5..000000000000 --- a/pythonFiles/testing_tools/run_adapter.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 deleted file mode 100644 index 95ca800b9885..000000000000 --- a/pythonFiles/testlauncher.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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 deleted file mode 100644 index e2e6976acd68..000000000000 --- a/pythonFiles/tests/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# 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 deleted file mode 100644 index 14086978c9af..000000000000 --- a/pythonFiles/tests/__main__.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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/test_install_debugpy.py b/pythonFiles/tests/debug_adapter/test_install_debugpy.py deleted file mode 100644 index 7a5354ba5ed7..000000000000 --- a/pythonFiles/tests/debug_adapter/test_install_debugpy.py +++ /dev/null @@ -1,36 +0,0 @@ -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/getJupyterVariableList.py b/pythonFiles/tests/ipython/getJupyterVariableList.py deleted file mode 100644 index d9b0778fa2d2..000000000000 --- a/pythonFiles/tests/ipython/getJupyterVariableList.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 deleted file mode 100644 index 6110d8653dee..000000000000 --- a/pythonFiles/tests/ipython/getJupyterVariableValue.py +++ /dev/null @@ -1,475 +0,0 @@ -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 deleted file mode 100644 index dde35fda3850..000000000000 --- a/pythonFiles/tests/ipython/random.csv +++ /dev/null @@ -1,6001 +0,0 @@ -".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 deleted file mode 100644 index e8ac420bf589..000000000000 --- a/pythonFiles/tests/ipython/scripts.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import re -import os -import json -import sys -import imp - - -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 deleted file mode 100644 index 6519607c2fbe..000000000000 --- a/pythonFiles/tests/ipython/test_variables.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -import sys -import os -import json -from .scripts import ( - get_variable_value, - get_variables, - get_data_frame_info, - get_data_frame_rows, - check_for_ipython, -) -import imp - -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 deleted file mode 100644 index ce5a62649962..000000000000 --- a/pythonFiles/tests/run_all.py +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index 37a2dce5cb7d..000000000000 --- a/pythonFiles/tests/test_normalize_for_interpreter.py +++ /dev/null @@ -1,122 +0,0 @@ -# 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/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 deleted file mode 100644 index 3501b9e118e5..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_okay(): - assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md b/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md deleted file mode 100644 index e30e96142d02..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md +++ /dev/null @@ -1,156 +0,0 @@ -## 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 deleted file mode 100644 index b8c495503895..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py +++ /dev/null @@ -1,51 +0,0 @@ -""" - -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/spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py +++ /dev/null @@ -1,3 +0,0 @@ - -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 deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py +++ /dev/null @@ -1,3 +0,0 @@ - -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 deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py +++ /dev/null @@ -1,3 +0,0 @@ - -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 deleted file mode 100644 index 27cccbdb77cc..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -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 deleted file mode 100644 index 4b51fde5667e..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt +++ /dev/null @@ -1,15 +0,0 @@ - -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 deleted file mode 100644 index e752106f503a..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py +++ /dev/null @@ -1,4 +0,0 @@ - - -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 deleted file mode 100644 index e9c675647f13..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 39d3ece9c0ba..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py +++ /dev/null @@ -1,227 +0,0 @@ -# ... - -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 deleted file mode 100644 index bd22d89f42bd..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index dd3e82535739..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 7ec91c783e2c..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py +++ /dev/null @@ -1,9 +0,0 @@ -''' -... -... -... -''' - - -def test_simple(): - assert True 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 deleted file mode 100644 index 18c92c09306e..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py +++ /dev/null @@ -1,9 +0,0 @@ - -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 deleted file mode 100644 index f3e7d9517631..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 6b6a01f87ec5..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 18cf56f90533..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 6a0b60d1d5bd..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py +++ /dev/null @@ -1,5 +0,0 @@ - - - -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 deleted file mode 100644 index 6a0b60d1d5bd..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py +++ /dev/null @@ -1,5 +0,0 @@ - - - -def test_simple(): - assert True 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 deleted file mode 100644 index bdb7e4fec3a5..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -... -""" - - -# ... - -ANSWER = 42 - - -def test_simple(): - assert True 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 deleted file mode 100644 index 4923c556c29a..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py +++ /dev/null @@ -1,8 +0,0 @@ - - -# ?!? -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 deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_simple(): - assert True 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 deleted file mode 100644 index 4c4134d75584..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_simple(): - assert True 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 deleted file mode 100644 index 54d6400a3465..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py +++ /dev/null @@ -1,7 +0,0 @@ - -def test_simple(): - assert True - - -# A syntax error: -: diff --git a/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py b/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py deleted file mode 100644 index f5d41f603be0..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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 deleted file mode 100644 index 2ca3955c6529..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py +++ /dev/null @@ -1,1399 +0,0 @@ -# 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: # 2.7 - from StringIO import StringIO -from os import name as OS_NAME -import sys -import unittest - -import pytest -import _pytest.doctest - -from ....util import Stub, StubProxy -from testing_tools.adapter.util import fix_path, fix_relpath, fix_fileid, PATH_JOIN -from testing_tools.adapter.info import TestInfo, TestPath, ParentInfo -from testing_tools.adapter.pytest import _pytest_item as pytest_item -from testing_tools.adapter.pytest._discovery import discover, TestCollector - -# 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(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(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(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(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") - - -class StubDoctestItem(StubSubtypedItem, _pytest.doctest.DoctestItem): - pass - - -class StubPytestSession(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(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 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 - - -class DiscoverTests(unittest.TestCase): - - DEFAULT_ARGS = [ - "--collect-only", - ] - - def test_basic(self): - stub = 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 = discover([], _pytest_main=stubpytest.main, _plugin=plugin) - - self.assertEqual(parents, []) - self.assertEqual(tests, expected) - self.assertEqual(stub.calls, calls) - - def test_failure(self): - stub = Stub() - pytest = StubPyTest(stub) - pytest.return_main = 2 - plugin = StubPlugin(stub) - - with self.assertRaises(Exception): - 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 = 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 = discover([], _pytest_main=pytest.main, _plugin=plugin) - - self.assertEqual(parents, []) - self.assertEqual(tests, expected) - self.assertEqual(stub.calls, calls) - - def test_stdio_hidden(self): - pytest_stdout = "spamspamspamspamspamspamspammityspam" - stub = Stub() - - def fake_pytest_main(args, plugins): - stub.add_call("pytest.main", None, {"args": args, "plugins": plugins}) - print(pytest_stdout, end="") - return 0 - - 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), - ] - - # 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: - discover([], hidestdio=True, _pytest_main=fake_pytest_main, _plugin=plugin) - finally: - sys.stdout = sys.__stdout__ - captured = buf.getvalue() - - self.assertEqual(captured, "") - self.assertEqual(stub.calls, calls) - - def test_stdio_not_hidden(self): - pytest_stdout = "spamspamspamspamspamspamspammityspam" - stub = Stub() - - def fake_pytest_main(args, plugins): - stub.add_call("pytest.main", None, {"args": args, "plugins": plugins}) - print(pytest_stdout, end="") - return 0 - - 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), - ] - - # 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: - discover([], hidestdio=False, _pytest_main=fake_pytest_main, _plugin=plugin) - finally: - sys.stdout = sys.__stdout__ - captured = buf.getvalue() - - self.assertEqual(captured, pytest_stdout) - self.assertEqual(stub.calls, calls) - - -class CollectorTests(unittest.TestCase): - def test_modifyitems(self): - stub = Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - config = StubPytestConfig(stub) - collector = TestCollector(tests=discovered) - - testroot = fix_path("/a/b/c") - relfile1 = fix_path("./test_spam.py") - relfile2 = fix_path("x/y/z/test_eggs.py") - - collector.pytest_collection_modifyitems( - session, - config, - [ - StubFunctionItem( - stub, - nodeid="test_spam.py::SpamTests::test_one", - name="test_one", - location=("test_spam.py", 12, "SpamTests.test_one"), - fspath=PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_one"), - ), - StubFunctionItem( - stub, - nodeid="test_spam.py::SpamTests::test_other", - name="test_other", - location=("test_spam.py", 19, "SpamTests.test_other"), - fspath=PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_other"), - ), - StubFunctionItem( - stub, - nodeid="test_spam.py::test_all", - name="test_all", - location=("test_spam.py", 144, "test_all"), - fspath=PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_all"), - ), - StubFunctionItem( - stub, - nodeid="test_spam.py::test_each[10-10]", - name="test_each[10-10]", - location=("test_spam.py", 273, "test_each[10-10]"), - fspath=PATH_JOIN(testroot, "test_spam.py"), - function=FakeFunc("test_each"), - ), - StubFunctionItem( - stub, - nodeid=relfile2 + "::All::BasicTests::test_first", - name="test_first", - location=(relfile2, 31, "All.BasicTests.test_first"), - fspath=PATH_JOIN(testroot, relfile2), - function=FakeFunc("test_first"), - ), - StubFunctionItem( - 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=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=TestInfo( - id="./test_spam.py::SpamTests::test_one", - name="test_one", - path=TestPath( - 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=TestInfo( - id="./test_spam.py::SpamTests::test_other", - name="test_other", - path=TestPath( - 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=TestInfo( - id="./test_spam.py::test_all", - name="test_all", - path=TestPath( - 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=TestInfo( - id="./test_spam.py::test_each[10-10]", - name="test_each[10-10]", - path=TestPath( - 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=TestInfo( - id="./x/y/z/test_eggs.py::All::BasicTests::test_first", - name="test_first", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile2), - func="All.BasicTests.test_first", - sub=None, - ), - source="{}:{}".format(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=TestInfo( - id="./x/y/z/test_eggs.py::All::BasicTests::test_each[1+2-3]", - name="test_each[1+2-3]", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile2), - func="All.BasicTests.test_each", - sub=["[1+2-3]"], - ), - source="{}:{}".format(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 = Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = fix_path("/a/b/c") - relfile = fix_path("x/y/z/test_eggs.py") - session.items = [ - StubFunctionItem( - stub, - nodeid=relfile + "::SpamTests::test_spam", - name="test_spam", - location=(relfile, 12, "SpamTests.test_spam"), - fspath=PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = 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=TestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam", - name="test_spam", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="SpamTests.test_spam", - sub=None, - ), - source="{}:{}".format(fix_relpath(relfile), 13), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests", - ), - ), - ), - ], - ) - - def test_doctest(self): - stub = Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = fix_path("/a/b/c") - doctestfile = fix_path("x/test_doctest.txt") - relfile = fix_path("x/y/z/test_eggs.py") - session.items = [ - StubDoctestItem( - stub, - nodeid=doctestfile + "::test_doctest.txt", - name="test_doctest.txt", - location=(doctestfile, 0, "[doctest] test_doctest.txt"), - fspath=PATH_JOIN(testroot, doctestfile), - ), - # With --doctest-modules - StubDoctestItem( - stub, - nodeid=relfile + "::test_eggs", - name="test_eggs", - location=(relfile, 0, "[doctest] test_eggs"), - fspath=PATH_JOIN(testroot, relfile), - ), - StubDoctestItem( - stub, - nodeid=relfile + "::test_eggs.TestSpam", - name="test_eggs.TestSpam", - location=(relfile, 12, "[doctest] test_eggs.TestSpam"), - fspath=PATH_JOIN(testroot, relfile), - ), - StubDoctestItem( - stub, - nodeid=relfile + "::test_eggs.TestSpam.TestEggs", - name="test_eggs.TestSpam.TestEggs", - location=(relfile, 27, "[doctest] test_eggs.TestSpam.TestEggs"), - fspath=PATH_JOIN(testroot, relfile), - ), - ] - collector = 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=TestInfo( - id="./x/test_doctest.txt::test_doctest.txt", - name="test_doctest.txt", - path=TestPath( - root=testroot, - relfile=fix_relpath(doctestfile), - func=None, - ), - source="{}:{}".format(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=TestInfo( - id="./x/y/z/test_eggs.py::test_eggs", - name="test_eggs", - path=TestPath( - root=testroot, relfile=fix_relpath(relfile), func=None, - ), - source="{}:{}".format(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=TestInfo( - id="./x/y/z/test_eggs.py::test_eggs.TestSpam", - name="test_eggs.TestSpam", - path=TestPath( - root=testroot, relfile=fix_relpath(relfile), func=None, - ), - source="{}:{}".format(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=TestInfo( - id="./x/y/z/test_eggs.py::test_eggs.TestSpam.TestEggs", - name="test_eggs.TestSpam.TestEggs", - path=TestPath( - root=testroot, relfile=fix_relpath(relfile), func=None, - ), - source="{}:{}".format(fix_relpath(relfile), 28), - markers=[], - parentid="./x/y/z/test_eggs.py", - ), - ), - ), - ], - ) - - def test_nested_brackets(self): - stub = Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = fix_path("/a/b/c") - relfile = fix_path("x/y/z/test_eggs.py") - session.items = [ - StubFunctionItem( - 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=PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = 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=TestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam[a-[b]-c]", - name="test_spam[a-[b]-c]", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="SpamTests.test_spam", - sub=["[a-[b]-c]"], - ), - source="{}:{}".format(fix_relpath(relfile), 13), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests::test_spam", - ), - ), - ), - ], - ) - - def test_nested_suite(self): - stub = Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = fix_path("/a/b/c") - relfile = fix_path("x/y/z/test_eggs.py") - session.items = [ - StubFunctionItem( - stub, - nodeid=relfile + "::SpamTests::Ham::Eggs::test_spam", - name="test_spam", - location=(relfile, 12, "SpamTests.Ham.Eggs.test_spam"), - fspath=PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = 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=TestInfo( - id="./x/y/z/test_eggs.py::SpamTests::Ham::Eggs::test_spam", - name="test_spam", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="SpamTests.Ham.Eggs.test_spam", - sub=None, - ), - source="{}:{}".format(fix_relpath(relfile), 13), - markers=None, - parentid="./x/y/z/test_eggs.py::SpamTests::Ham::Eggs", - ), - ), - ), - ], - ) - - def test_windows(self): - stub = 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: - StubFunctionItem( - 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( - StubFunctionItem( - stub, - nodeid=fileid + "::test_spam", - name="test_spam", - location=(locfile, 12, "test_spam"), - fspath=fspath, - function=FakeFunc("test_spam"), - ) - ) - collector = 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=TestInfo( - id=r"./X/Y/Z/test_Eggs.py::SpamTests::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_a.py::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_b.py::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_c.py::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_d.py::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_e.py::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_f.py::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_g.py::test_spam", - name="test_spam", - path=TestPath( - 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=TestInfo( - id=r"./X/test_h.py::test_spam", - name="test_spam", - path=TestPath( - 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 = Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = fix_path("/a/b/c") - relfile = fix_path("x/y/z/test_eggs.py") - session.items = [ - StubFunctionItem( - stub, - nodeid=relfile + "::SpamTests::()::()::test_spam", - name="test_spam", - location=(relfile, 12, "SpamTests.test_spam"), - fspath=PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = 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=TestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam", - name="test_spam", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="SpamTests.test_spam", - sub=[], - ), - source="{}:{}".format(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 = Stub() - discovered = StubDiscoveredTests(stub) - session = StubPytestSession(stub) - testroot = fix_path("/a/b/c") - relfile = fix_path("x/y/z/test_eggs.py") - srcfile = fix_path("x/y/z/_extern.py") - session.items = [ - StubFunctionItem( - stub, - nodeid=relfile + "::SpamTests::test_spam", - name="test_spam", - location=(srcfile, 12, "SpamTests.test_spam"), - fspath=PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - StubFunctionItem( - stub, - nodeid=relfile + "::test_ham", - name="test_ham", - location=(srcfile, 3, "test_ham"), - fspath=PATH_JOIN(testroot, relfile), - function=FakeFunc("test_spam"), - ), - ] - collector = 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=TestInfo( - id="./x/y/z/test_eggs.py::SpamTests::test_spam", - name="test_spam", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="SpamTests.test_spam", - sub=None, - ), - source="{}:{}".format(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=TestInfo( - id="./x/y/z/test_eggs.py::test_ham", - name="test_ham", - path=TestPath( - root=testroot, - relfile=fix_relpath(relfile), - func="test_ham", - sub=None, - ), - source="{}:{}".format(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 deleted file mode 100644 index e14b08ee9d5b..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test___main__.py +++ /dev/null @@ -1,193 +0,0 @@ -# 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 deleted file mode 100644 index e1238282cad7..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_discovery.py +++ /dev/null @@ -1,614 +0,0 @@ -# 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 TestInfo, TestPath, 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 = [ - TestInfo( - # missing "./": - id="test_spam.py::test_each[10-10]", - name="test_each[10-10]", - path=TestPath( - root=testroot, relfile=relfile, func="test_each", sub=["[10-10]"], - ), - source="{}:{}".format(relfile, 10), - markers=None, - # missing "./": - parentid="test_spam.py::test_each", - ), - TestInfo( - id="test_spam.py::All::BasicTests::test_first", - name="test_first", - path=TestPath( - 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( - TestInfo( - id="./test_spam.py::test_each", - name="test_each", - path=TestPath(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 = [ - TestInfo( - # missing "./", using pathsep: - id=relfile + "::test_each[10-10]", - name="test_each[10-10]", - path=TestPath( - 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", - ), - TestInfo( - # missing "./", using pathsep: - id=relfile + "::All::BasicTests::test_first", - name="test_first", - path=TestPath( - 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 = TestInfo( - # missing "./": - id=relfile + "::test_spam", - name="test_spam", - path=TestPath( - 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 = [ - TestInfo( - # missing "./": - id=relfile1 + "::test_spam", - name="test_spam", - path=TestPath( - 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( - [ - TestInfo( - id=relfile2 + "::BasicTests::test_first", - name="test_first", - path=TestPath( - 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 - TestInfo( - id="./test_spam.py::test_spam", - name="test_spam", - path=TestPath( - root=testroot1, relfile=fix_relpath(relfile1), func="test_spam", - ), - source="{}:{}".format(relfile1, 10), - markers=[], - parentid="./test_spam.py", - ), - # the secondroot - TestInfo( - id="./w/test_eggs.py::BasicTests::test_first", - name="test_first", - path=TestPath( - 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 = [ - TestInfo( - id=doctestfile + "::test_doctest.txt", - name="test_doctest.txt", - path=TestPath(root=testroot, relfile=doctestfile, func=None,), - source="{}:{}".format(doctestfile, 0), - markers=[], - parentid=doctestfile, - ), - # With --doctest-modules - TestInfo( - id=relfile + "::test_eggs", - name="test_eggs", - path=TestPath(root=testroot, relfile=relfile, func=None,), - source="{}:{}".format(relfile, 0), - markers=[], - parentid=relfile, - ), - TestInfo( - id=relfile + "::test_eggs.TestSpam", - name="test_eggs.TestSpam", - path=TestPath(root=testroot, relfile=relfile, func=None,), - source="{}:{}".format(relfile, 12), - markers=[], - parentid=relfile, - ), - TestInfo( - id=relfile + "::test_eggs.TestSpam.TestEggs", - name="test_eggs.TestSpam.TestEggs", - path=TestPath(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 = [ - TestInfo( - id=relfile + "::TestOuter::TestInner::test_spam", - name="test_spam", - path=TestPath( - root=testroot, - relfile=relfile, - func="TestOuter.TestInner.test_spam", - ), - source="{}:{}".format(relfile, 10), - markers=None, - parentid=relfile + "::TestOuter::TestInner", - ), - TestInfo( - id=relfile + "::TestOuter::TestInner::test_eggs", - name="test_eggs", - path=TestPath( - 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 deleted file mode 100644 index ce5bd120e3fa..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_functional.py +++ /dev/null @@ -1,1529 +0,0 @@ -# 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 - - -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. - - -@pytest.mark.functional -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 deleted file mode 100644 index 0c94ebd51567..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_report.py +++ /dev/null @@ -1,1113 +0,0 @@ -# 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 TestInfo, TestPath, 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 = [ - TestInfo( - id="test#1", - name="test_spam", - path=TestPath(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 = [ - TestInfo( - id=relfileid1 + "::test_spam", - name="test_spam", - path=TestPath(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( - [ - TestInfo( - id=relfileid2 + "::BasicTests::test_first", - name="test_first", - path=TestPath( - 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 = [ - TestInfo( - id=relfileid1 + "::MySuite::test_x1", - name="test_x1", - path=TestPath( - root=testroot, relfile=fix_path(relfileid1), func="MySuite.test_x1", - ), - source="{}:{}".format(fix_path(relfileid1), 10), - markers=None, - parentid=relfileid1 + "::MySuite", - ), - TestInfo( - id=relfileid1 + "::MySuite::test_x2", - name="test_x2", - path=TestPath( - root=testroot, relfile=fix_path(relfileid1), func="MySuite.test_x2", - ), - source="{}:{}".format(fix_path(relfileid1), 21), - markers=None, - parentid=relfileid1 + "::MySuite", - ), - TestInfo( - id=relfileid2 + "::SpamTests::test_okay", - name="test_okay", - path=TestPath( - root=testroot, - relfile=fix_path(relfileid2), - func="SpamTests.test_okay", - ), - source="{}:{}".format(fix_path(relfileid2), 17), - markers=None, - parentid=relfileid2 + "::SpamTests", - ), - TestInfo( - id=relfileid3 + "::test_ham1", - name="test_ham1", - path=TestPath( - root=testroot, relfile=fix_path(relfileid3), func="test_ham1", - ), - source="{}:{}".format(fix_path(relfileid3), 8), - markers=None, - parentid=relfileid3, - ), - TestInfo( - id=relfileid3 + "::HamTests::test_uh_oh", - name="test_uh_oh", - path=TestPath( - root=testroot, - relfile=fix_path(relfileid3), - func="HamTests.test_uh_oh", - ), - source="{}:{}".format(fix_path(relfileid3), 19), - markers=["expected-failure"], - parentid=relfileid3 + "::HamTests", - ), - TestInfo( - id=relfileid3 + "::HamTests::test_whoa", - name="test_whoa", - path=TestPath( - root=testroot, - relfile=fix_path(relfileid3), - func="HamTests.test_whoa", - ), - source="{}:{}".format(fix_path(relfileid3), 35), - markers=None, - parentid=relfileid3 + "::HamTests", - ), - TestInfo( - id=relfileid3 + "::MoreHam::test_yay[1-2]", - name="test_yay[1-2]", - path=TestPath( - 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", - ), - TestInfo( - id=relfileid3 + "::MoreHam::test_yay[1-2][3-4]", - name="test_yay[1-2][3-4]", - path=TestPath( - 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]", - ), - TestInfo( - id=relfileid4 + "::SpamTests::test_okay", - name="test_okay", - path=TestPath( - root=testroot, - relfile=fix_path(relfileid4), - func="SpamTests.test_okay", - ), - source="{}:{}".format(fix_path(relfileid4), 15), - markers=None, - parentid=relfileid4 + "::SpamTests", - ), - TestInfo( - id=relfileid5 + "::SpamTests::test_okay", - name="test_okay", - path=TestPath( - root=testroot, - relfile=fix_path(relfileid5), - func="SpamTests.test_okay", - ), - source="{}:{}".format(fix_path(relfileid5), 12), - markers=None, - parentid=relfileid5 + "::SpamTests", - ), - TestInfo( - id=relfileid6 + "::SpamTests::test_okay", - name="test_okay", - path=TestPath( - 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 = [ - TestInfo( - id="test#1", - name="test_spam_1", - path=TestPath( - 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 - TestInfo( - id="test#1", - name="test_x1", - path=TestPath( - root=testroot1, relfile=relfile1, func="MySuite.test_x1", sub=None, - ), - source="{}:{}".format(relfile1, 10), - markers=None, - parentid="suite#1", - ), - TestInfo( - id="test#2", - name="test_x2", - path=TestPath( - root=testroot1, relfile=relfile1, func="MySuite.test_x2", sub=None, - ), - source="{}:{}".format(relfile1, 21), - markers=None, - parentid="suite#1", - ), - # under second root folder - TestInfo( - id="test#3", - name="test_okay", - path=TestPath( - root=testroot2, - relfile=relfile2, - func="SpamTests.test_okay", - sub=None, - ), - source="{}:{}".format(relfile2, 17), - markers=None, - parentid="suite#2", - ), - TestInfo( - id="test#4", - name="test_ham1", - path=TestPath( - root=testroot2, relfile=relfile3, func="test_ham1", sub=None, - ), - source="{}:{}".format(relfile3, 8), - markers=None, - parentid="file#3", - ), - TestInfo( - id="test#5", - name="test_uh_oh", - path=TestPath( - root=testroot2, - relfile=relfile3, - func="HamTests.test_uh_oh", - sub=None, - ), - source="{}:{}".format(relfile3, 19), - markers=["expected-failure"], - parentid="suite#3", - ), - TestInfo( - id="test#6", - name="test_whoa", - path=TestPath( - root=testroot2, - relfile=relfile3, - func="HamTests.test_whoa", - sub=None, - ), - source="{}:{}".format(relfile3, 35), - markers=None, - parentid="suite#3", - ), - TestInfo( - id="test#7", - name="test_yay (sub1)", - path=TestPath( - root=testroot2, - relfile=relfile3, - func="MoreHam.test_yay", - sub=["sub1"], - ), - source="{}:{}".format(relfile3, 57), - markers=None, - parentid="suite#4", - ), - TestInfo( - id="test#8", - name="test_yay (sub2) (sub3)", - path=TestPath( - root=testroot2, - relfile=relfile3, - func="MoreHam.test_yay", - sub=["sub2", "sub3"], - ), - source="{}:{}".format(relfile3, 72), - markers=None, - parentid="suite#3", - ), - TestInfo( - id="test#9", - name="test_okay", - path=TestPath( - root=testroot2, - relfile=relfile4, - func="SpamTests.test_okay", - sub=None, - ), - source="{}:{}".format(relfile4, 15), - markers=None, - parentid="suite#5", - ), - TestInfo( - id="test#10", - name="test_okay", - path=TestPath( - root=testroot2, - relfile=relfile5, - func="SpamTests.test_okay", - sub=None, - ), - source="{}:{}".format(relfile5, 12), - markers=None, - parentid="suite#6", - ), - TestInfo( - id="test#11", - name="test_okay", - path=TestPath( - 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 deleted file mode 100644 index e9c0e5f2ab19..000000000000 --- a/pythonFiles/tests/testing_tools/adapter/test_util.py +++ /dev/null @@ -1,320 +0,0 @@ -# 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 - -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): - @pytest.mark.functional - 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, ".\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", "\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 deleted file mode 100644 index 45c3536145cf..000000000000 --- a/pythonFiles/tests/util.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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 deleted file mode 100644 index 7731b63b7e65..000000000000 --- a/pythonFiles/visualstudio_py_testlauncher.py +++ /dev/null @@ -1,393 +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" - -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 [

+ +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/resources/report_issue_user_data_template.md b/resources/report_issue_user_data_template.md new file mode 100644 index 000000000000..037b844511d3 --- /dev/null +++ b/resources/report_issue_user_data_template.md @@ -0,0 +1,21 @@ +- Python version (& distribution if applicable, e.g. Anaconda): {0} +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): {1} +- Value of the `python.languageServer` setting: {2} + +
+User Settings +

+ +``` +{3}{4} +``` +

+
+ +
+Installed Extensions + +|Extension Name|Extension Id|Version| +|---|---|---| +{5} +
diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json new file mode 100644 index 000000000000..7e034651c46d --- /dev/null +++ b/resources/report_issue_user_settings.json @@ -0,0 +1,99 @@ +{ + "initialize": false, + "pythonPath": "placeholder", + "onDidChange": false, + "defaultInterpreterPath": "placeholder", + "defaultLS": false, + "envFile": "placeholder", + "venvPath": "placeholder", + "venvFolders": "placeholder", + "activeStateToolPath": "placeholder", + "condaPath": "placeholder", + "pipenvPath": "placeholder", + "poetryPath": "placeholder", + "pixiToolPath": "placeholder", + "devOptions": false, + "globalModuleInstallation": false, + "languageServer": true, + "languageServerIsDefault": false, + "logging": true, + "useIsolation": false, + "changed": false, + "_pythonPath": false, + "_defaultInterpreterPath": false, + "workspace": false, + "workspaceRoot": false, + "linting": { + "enabled": true, + "cwd": "placeholder", + "flake8Args": "placeholder", + "flake8CategorySeverity": false, + "flake8Enabled": true, + "flake8Path": "placeholder", + "ignorePatterns": false, + "lintOnSave": true, + "maxNumberOfProblems": false, + "banditArgs": "placeholder", + "banditEnabled": true, + "banditPath": "placeholder", + "mypyArgs": "placeholder", + "mypyCategorySeverity": false, + "mypyEnabled": true, + "mypyPath": "placeholder", + "pycodestyleArgs": "placeholder", + "pycodestyleCategorySeverity": false, + "pycodestyleEnabled": true, + "pycodestylePath": "placeholder", + "prospectorArgs": "placeholder", + "prospectorEnabled": true, + "prospectorPath": "placeholder", + "pydocstyleArgs": "placeholder", + "pydocstyleEnabled": true, + "pydocstylePath": "placeholder", + "pylamaArgs": "placeholder", + "pylamaEnabled": true, + "pylamaPath": "placeholder", + "pylintArgs": "placeholder", + "pylintCategorySeverity": false, + "pylintEnabled": false, + "pylintPath": "placeholder" + }, + "analysis": { + "completeFunctionParens": true, + "autoImportCompletions": true, + "autoSearchPaths": "placeholder", + "stubPath": "placeholder", + "diagnosticMode": true, + "extraPaths": "placeholder", + "useLibraryCodeForTypes": true, + "typeCheckingMode": true, + "memory": true, + "symbolsHierarchyDepthLimit": false + }, + "testing": { + "cwd": "placeholder", + "debugPort": true, + "promptToConfigure": true, + "pytestArgs": "placeholder", + "pytestEnabled": true, + "pytestPath": "placeholder", + "unittestArgs": "placeholder", + "unittestEnabled": true, + "autoTestDiscoverOnSaveEnabled": true, + "autoTestDiscoverOnSavePattern": "placeholder" + }, + "terminal": { + "activateEnvironment": true, + "executeInFileDir": "placeholder", + "launchArgs": "placeholder", + "activateEnvInCurrentTerminal": false + }, + "tensorBoard": { + "logDirectory": "placeholder" + }, + "experiments": { + "enabled": true, + "optInto": true, + "optOutFrom": true + } +} diff --git a/resources/walkthrough/create-environment.svg b/resources/walkthrough/create-environment.svg new file mode 100644 index 000000000000..bb48e1b16711 --- /dev/null +++ b/resources/walkthrough/create-environment.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/create-notebook.svg b/resources/walkthrough/create-notebook.svg new file mode 100644 index 000000000000..05dadc0cc6de --- /dev/null +++ b/resources/walkthrough/create-notebook.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/data-science.svg b/resources/walkthrough/data-science.svg new file mode 100644 index 000000000000..506bed2161b1 --- /dev/null +++ b/resources/walkthrough/data-science.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/environments-info.md b/resources/walkthrough/environments-info.md new file mode 100644 index 000000000000..7bdc61a96e2e --- /dev/null +++ b/resources/walkthrough/environments-info.md @@ -0,0 +1,10 @@ +## Python Environments + +Create Environment Dropdown + +Python virtual environments are considered a best practice in Python development. A virtual environment includes a Python interpreter and any packages you have installed into it, such as numpy or Flask. + +After you create a virtual environment using the **Python: Create Environment** command, you can install packages into the environment. +For example, type `python -m pip install numpy` in an activated terminal to install `numpy` into the environment. + +🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more. diff --git a/resources/walkthrough/install-python-linux.md b/resources/walkthrough/install-python-linux.md new file mode 100644 index 000000000000..78a12870799f --- /dev/null +++ b/resources/walkthrough/install-python-linux.md @@ -0,0 +1,22 @@ +# Install Python on Linux + +To install the latest version of Python on [Debian-based Linux distributions](https://www.debian.org/), you can create a new terminal (Ctrl + Shift + `) and run the following commands: + + +``` +sudo apt-get update +sudo apt-get install python3 python3-venv python3-pip +``` + +For [Fedora-based Linux distributions](https://getfedora.org/), you can run the following: + +``` +sudo dnf install python3 +``` + +To verify if Python was successfully installed, run the following command in the terminal: + + +``` +python3 --version +``` diff --git a/resources/walkthrough/install-python-macos.md b/resources/walkthrough/install-python-macos.md new file mode 100644 index 000000000000..470d682d4eb2 --- /dev/null +++ b/resources/walkthrough/install-python-macos.md @@ -0,0 +1,15 @@ +# Install Python on macOS + +If you have [Homebrew](https://brew.sh/) installed, you can install Python by running the following command in the terminal (Ctrl + Shift + `): + +``` +brew install python3 +``` + +If you don't have Homebrew, you can download a Python installer for macOS from [python.org](https://www.python.org/downloads/mac-osx/). + +To verify if Python was successfully installed, run the following command in the terminal: + +``` +python3 --version +``` diff --git a/resources/walkthrough/install-python-windows-8.md b/resources/walkthrough/install-python-windows-8.md new file mode 100644 index 000000000000..f25f2f7d024d --- /dev/null +++ b/resources/walkthrough/install-python-windows-8.md @@ -0,0 +1,15 @@ +## Install Python on Windows + +If you don't have Python installed on your Windows machine, you can install it [from python.org](https://www.python.org/downloads). + +To verify it's installed, create a new terminal (Ctrl + Shift + `) and try running the following command: + +``` +python --version +``` + +You should see something similar to the following: +``` +Python 3.9.5 +``` +For additional information about using Python on Windows, see [Using Python on Windows at Python.org](https://docs.python.org/3.10/using/windows.html). diff --git a/resources/walkthrough/interactive-window.svg b/resources/walkthrough/interactive-window.svg new file mode 100644 index 000000000000..83446ed8e66a --- /dev/null +++ b/resources/walkthrough/interactive-window.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/learnmore.svg b/resources/walkthrough/learnmore.svg new file mode 100644 index 000000000000..c5fd67e75471 --- /dev/null +++ b/resources/walkthrough/learnmore.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/walkthrough/open-folder.svg b/resources/walkthrough/open-folder.svg new file mode 100644 index 000000000000..1615718a83dd --- /dev/null +++ b/resources/walkthrough/open-folder.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/play-button-dark.png b/resources/walkthrough/play-button-dark.png new file mode 100644 index 000000000000..113ad62b87c2 Binary files /dev/null and b/resources/walkthrough/play-button-dark.png differ diff --git a/resources/walkthrough/python-interpreter.svg b/resources/walkthrough/python-interpreter.svg new file mode 100644 index 000000000000..0f6e262321ec --- /dev/null +++ b/resources/walkthrough/python-interpreter.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/rundebug2.svg b/resources/walkthrough/rundebug2.svg new file mode 100644 index 000000000000..6d1fe753cc4f --- /dev/null +++ b/resources/walkthrough/rundebug2.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/schemas/conda-environment.json b/schemas/conda-environment.json index 824174948ff8..fb1e821778c3 100644 --- a/schemas/conda-environment.json +++ b/schemas/conda-environment.json @@ -1,7 +1,7 @@ { "title": "conda environment file", - "description": "Support for conda's enviroment.yml files (e.g. `conda env export > environment.yml`)", - "id": "https://raw.githubusercontent.com/Microsoft/vscode-python/master/schemas/conda-environment.json", + "description": "Support for conda's environment.yml files (e.g. `conda env export > environment.yml`)", + "id": "https://raw.githubusercontent.com/Microsoft/vscode-python/main/schemas/conda-environment.json", "$schema": "http://json-schema.org/draft-04/schema#", "definitions": { "channel": { diff --git a/schemas/condarc.json b/schemas/condarc.json index 00ae69dee929..a881315d3137 100644 --- a/schemas/condarc.json +++ b/schemas/condarc.json @@ -1,7 +1,7 @@ { "title": ".condarc", "description": "The conda configuration file; https://conda.io/docs/user-guide/configuration/use-condarc.html", - "id": "https://raw.githubusercontent.com/Microsoft/vscode-python/master/schemas/condarc.json", + "id": "https://raw.githubusercontent.com/Microsoft/vscode-python/main/schemas/condarc.json", "$schema": "http://json-schema.org/draft-04/schema#", "definitions": { "channel": { @@ -59,7 +59,14 @@ } }, "ssl_verify": { - "type": "boolean" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "offline": { "type": "boolean" diff --git a/scripts/cleanup-eslintignore.js b/scripts/cleanup-eslintignore.js new file mode 100644 index 000000000000..848f5a9c4910 --- /dev/null +++ b/scripts/cleanup-eslintignore.js @@ -0,0 +1,44 @@ +const fs = require('fs'); +const path = require('path'); + +const baseDir = process.cwd(); +const eslintignorePath = path.join(baseDir, '.eslintignore'); + +fs.readFile(eslintignorePath, 'utf8', (err, data) => { + if (err) { + console.error('Error reading .eslintignore file:', err); + return; + } + + const lines = data.split('\n'); + const files = lines.map((line) => line.trim()).filter((line) => line && !line.startsWith('#')); + const nonExistentFiles = []; + + files.forEach((file) => { + const filePath = path.join(baseDir, file); + if (!fs.existsSync(filePath) && file !== 'pythonExtensionApi/out/') { + nonExistentFiles.push(file); + } + }); + + if (nonExistentFiles.length > 0) { + console.log('The following files listed in .eslintignore do not exist:'); + nonExistentFiles.forEach((file) => console.log(file)); + + const updatedLines = lines.filter((line) => { + const trimmedLine = line.trim(); + return !nonExistentFiles.includes(trimmedLine) || trimmedLine === 'pythonExtensionApi/out/'; + }); + const updatedData = `${updatedLines.join('\n')}\n`; + + fs.writeFile(eslintignorePath, updatedData, 'utf8', (err) => { + if (err) { + console.error('Error writing to .eslintignore file:', err); + return; + } + console.log('Non-existent files have been removed from .eslintignore.'); + }); + } else { + console.log('All files listed in .eslintignore exist.'); + } +}); diff --git a/scripts/issue_velocity_summary_script.py b/scripts/issue_velocity_summary_script.py new file mode 100644 index 000000000000..94929d1798a9 --- /dev/null +++ b/scripts/issue_velocity_summary_script.py @@ -0,0 +1,110 @@ +""" +This script fetches open issues from the microsoft/vscode-python repository, +calculates the thumbs-up per day for each issue, and generates a markdown +summary of the issues sorted by highest thumbs-up per day. Issues with zero +thumbs-up are excluded from the summary. +""" + +import requests +import os +from datetime import datetime, timezone + + +GITHUB_API_URL = "https://api.github.com" +REPO = "microsoft/vscode-python" +TOKEN = os.getenv("GITHUB_TOKEN") + + +def fetch_issues(): + """ + Fetches all open issues from the specified GitHub repository. + + Returns: + list: A list of dictionaries representing the issues. + """ + headers = {"Authorization": f"token {TOKEN}"} + issues = [] + page = 1 + while True: + query = ( + f"{GITHUB_API_URL}/repos/{REPO}/issues?state=open&per_page=25&page={page}" + ) + response = requests.get(query, headers=headers) + if response.status_code == 403: + raise Exception( + "Access forbidden: Check your GitHub token and permissions." + ) + response.raise_for_status() + page_issues = response.json() + if not page_issues: + break + issues.extend(page_issues) + page += 1 + return issues + + +def calculate_thumbs_up_per_day(issue): + """ + Calculates the thumbs-up per day for a given issue. + + Args: + issue (dict): A dictionary representing the issue. + + Returns: + float: The thumbs-up per day for the issue. + """ + created_at = datetime.strptime(issue["created_at"], "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + now = datetime.now(timezone.utc) + days_open = (now - created_at).days or 1 + thumbs_up = issue["reactions"].get("+1", 0) + return thumbs_up / days_open + + +def generate_markdown_summary(issues): + """ + Generates a markdown summary of the issues. + + Args: + issues (list): A list of dictionaries representing the issues. + + Returns: + str: A markdown-formatted string summarizing the issues. + """ + summary = "| URL | Title | 👍 | Days Open | 👍/day |\n| --- | ----- | --- | --------- | ------ |\n" + issues_with_thumbs_up = [] + for issue in issues: + created_at = datetime.strptime( + issue["created_at"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=timezone.utc) + now = datetime.now(timezone.utc) + days_open = (now - created_at).days or 1 + thumbs_up = issue["reactions"].get("+1", 0) + if thumbs_up > 0: + thumbs_up_per_day = thumbs_up / days_open + issues_with_thumbs_up.append( + (issue, thumbs_up, days_open, thumbs_up_per_day) + ) + + # Sort issues by thumbs_up_per_day in descending order + issues_with_thumbs_up.sort(key=lambda x: x[3], reverse=True) + + for issue, thumbs_up, days_open, thumbs_up_per_day in issues_with_thumbs_up: + summary += f"| {issue['html_url']} | {issue['title']} | {thumbs_up} | {days_open} | {thumbs_up_per_day:.2f} |\n" + + return summary + + +def main(): + """ + Main function to fetch issues, generate the markdown summary, and write it to a file. + """ + issues = fetch_issues() + summary = generate_markdown_summary(issues) + with open("endorsement_velocity_summary.md", "w") as f: + f.write(summary) + + +if __name__ == "__main__": + main() diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh new file mode 100644 index 000000000000..3d473d1ee172 --- /dev/null +++ b/scripts/onCreateCommand.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Install pyenv and Python versions here to avoid using shim. +curl https://pyenv.run | bash +echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc +echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc +# echo 'eval "$(pyenv init -)"' >> ~/.bashrc + +export PYENV_ROOT="$HOME/.pyenv" +command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" +# eval "$(pyenv init -)" Comment this out and DO NOT use shim. +source ~/.bashrc + +# Install Python via pyenv . +pyenv install 3.8.18 3.9:latest 3.10:latest 3.11:latest + +# Set default Python version to 3.8 . +pyenv global 3.8.18 + +npm ci + +# Create Virutal environment. +pyenv exec python -m venv .venv + +# Activate Virtual environment. +source /workspaces/vscode-python/.venv/bin/activate + +# Install required Python libraries. +/workspaces/vscode-python/.venv/bin/python -m pip install nox +nox --session install_python_libs + +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt + +# Below will crash codespace +# npm run compile diff --git a/snippets/python.json b/snippets/python.json deleted file mode 100644 index 11b39a30ccef..000000000000 --- a/snippets/python.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "if": { - "prefix": "if", - "body": ["if ${1:expression}:", "\t${2:pass}"], - "description": "Code snippet for an if statement" - }, - "if/else": { - "prefix": "if/else", - "body": ["if ${1:condition}:", "\t${2:pass}", "else:", "\t${3:pass}"], - "description": "Code snippet for an if statement with else" - }, - "elif": { - "prefix": "elif", - "body": ["elif ${1:expression}:", "\t${2:pass}"], - "description": "Code snippet for an elif" - }, - "else": { - "prefix": "else", - "body": ["else:", "\t${1:pass}"], - "description": "Code snippet for an else" - }, - "while": { - "prefix": "while", - "body": ["while ${1:expression}:", "\t${2:pass}"], - "description": "Code snippet for a while loop" - }, - "while/else": { - "prefix": "while/else", - "body": ["while ${1:expression}:", "\t${2:pass}", "else:", "\t${3:pass}"], - "description": "Code snippet for a while loop with else" - }, - "for": { - "prefix": "for", - "body": ["for ${1:target_list} in ${2:expression_list}:", "\t${3:pass}"], - "description": "Code snippet for a for loop" - }, - "for/else": { - "prefix": "for/else", - "body": ["for ${1:target_list} in ${2:expression_list}:", "\t${3:pass}", "else:", "\t${4:pass}"], - "description": "Code snippet for a for loop with else" - }, - "try/except": { - "prefix": "try/except", - "body": ["try:", "\t${1:pass}", "except ${2:expression} as ${3:identifier}:", "\t${4:pass}"], - "description": "Code snippet for a try/except statement" - }, - "try/finally": { - "prefix": "try/finally", - "body": ["try:", "\t${1:pass}", "finally:", "\t${2:pass}"], - "description": "Code snippet for a try/finally statement" - }, - "try/except/else": { - "prefix": "try/except/else", - "body": [ - "try:", - "\t${1:pass}", - "except ${2:expression} as ${3:identifier}:", - "\t${4:pass}", - "else:", - "\t${5:pass}" - ], - "description": "Code snippet for a try/except/else statement" - }, - "try/except/finally": { - "prefix": "try/except/finally", - "body": [ - "try:", - "\t${1:pass}", - "except ${2:expression} as ${3:identifier}:", - "\t${4:pass}", - "finally:", - "\t${5:pass}" - ], - "description": "Code snippet for a try/except/finally statement" - }, - "try/except/else/finally": { - "prefix": "try/except/else/finally", - "body": [ - "try:", - "\t${1:pass}", - "except ${2:expression} as ${3:identifier}:", - "\t${4:pass}", - "else:", - "\t${5:pass}", - "finally:", - "\t${6:pass}" - ], - "description": "Code snippet for a try/except/else/finally statement" - }, - "with": { - "prefix": "with", - "body": ["with ${1:expression} as ${2:target}:", "\t${3:pass}"], - "description": "Code snippet for a with statement" - }, - "def": { - "prefix": "def", - "body": ["def ${1:funcname}(${2:parameter_list}):", "\t${3:pass}"], - "description": "Code snippet for a function definition" - }, - "def(class method)": { - "prefix": "def(class method)", - "body": ["def ${1:funcname}(self, ${2:parameter_list}):", "\t${3:pass}"], - "description": "Code snippet for a class method" - }, - "def(static class method)": { - "prefix": "def(static class method)", - "body": ["@staticmethod", "def ${1:funcname}(${2:parameter_list}):", "\t${3:pass}"], - "description": "Code snippet for a static class method" - }, - "def(abstract class method)": { - "prefix": "def(abstract class method)", - "body": ["def ${1:funcname}(self, ${2:parameter_list}):", "\traise NotImplementedError"], - "description": "Code snippet for an abstract class method" - }, - "class": { - "prefix": "class", - "body": ["class ${1:classname}(${2:object}):", "\t${3:pass}"], - "description": "Code snippet for a class definition" - }, - "lambda": { - "prefix": "lambda", - "body": ["lambda ${1:parameter_list}: ${2:expression}"], - "description": "Code snippet for a lambda statement" - }, - "if(main)": { - "prefix": "__main__", - "body": ["if __name__ == \"__main__\":", " ${1:pass}"], - "description": "Code snippet for a `if __name__ == \"__main__\": ...` block" - }, - "async/def": { - "prefix": "async/def", - "body": ["async def ${1:funcname}(${2:parameter_list}):", "\t${3:pass}"], - "description": "Code snippet for an async statement" - }, - "async/for": { - "prefix": "async/for", - "body": ["async for ${1:target} in ${2:iter}:", "\t${3:block}"], - "description": "Code snippet for an async for statement" - }, - "async/for/else": { - "prefix": "async/for/else", - "body": ["async for ${1:target} in ${2:iter}:", "\t${3:block}", "else:", "\t${4:block}"], - "description": "Code snippet for an async for statement with else" - }, - "async/with": { - "prefix": "async/with", - "body": ["async with ${1:expr} as ${2:var}:", "\t${3:block}"], - "description": "Code snippet for an async with statement" - }, - "ipdb": { - "prefix": "ipdb", - "body": "import ipdb; ipdb.set_trace()", - "description": "Code snippet for ipdb debug" - }, - "pdb": { - "prefix": "pdb", - "body": "import pdb; pdb.set_trace()", - "description": "Code snippet for pdb debug" - }, - "pudb": { - "prefix": "pudb", - "body": "import pudb; pudb.set_trace()", - "description": "Code snippet for pudb debug" - }, - "add/new/cell": { - "prefix": "add/new/cell", - "body": "# %%", - "description": "Code snippet to add a new cell" - }, - "mark/markdown": { - "prefix": "mark/markdown", - "body": "# %% [markdown]", - "description": "Code snippet to add a new markdown cell" - } -} diff --git a/sprint-planning.github-issues b/sprint-planning.github-issues old mode 100755 new mode 100644 index 73c40eae0253..1fbd09a790e8 --- a/sprint-planning.github-issues +++ b/sprint-planning.github-issues @@ -2,97 +2,71 @@ { "kind": 1, "language": "markdown", - "value": "# Query constants", - "editable": true + "value": "# Query constants" }, { "kind": 2, "language": "github-issues", - "value": "$pvsc=repo:microsoft/vscode-python\n$not_DS=-label:\"data science\"\n$open=is:open", - "editable": true + "value": "$pvsc=repo:microsoft/vscode-python\n$open=is:open\n$upvotes=sort:reactions-+1-desc" }, { "kind": 1, "language": "markdown", - "value": "# Priority issues 🚨", - "editable": true + "value": "# Priority issues 🚨" }, { "kind": 1, "language": "markdown", - "value": "## P0", - "editable": true + "value": "## Important/P1" }, { "kind": 2, "language": "github-issues", - "value": "$pvsc $not_DS $open label:\"P0\"", - "editable": true + "value": "$pvsc $open label:\"important\"" }, { "kind": 1, "language": "markdown", - "value": "## P1", - "editable": true + "value": "# Regressions 🔙" }, { "kind": 2, "language": "github-issues", - "value": "$pvsc $not_DS $open label:\"P1\"", - "editable": true + "value": "$pvsc $open label:\"regression\"" }, { "kind": 1, "language": "markdown", - "value": "# Regressions 🔙", - "editable": true + "value": "# Partner asks" }, { "kind": 2, "language": "github-issues", - "value": "$pvsc $not_DS $open label:\"reason-regression\"", - "editable": true + "value": "$pvsc $open label:\"partner ask\"" }, { "kind": 1, "language": "markdown", - "value": "# Partner asks", - "editable": true - }, - { - "kind": 2, - "language": "github-issues", - "value": "$pvsc $not_DS $open label:\"partner ask\"", - "editable": true - }, - { - "kind": 1, - "language": "markdown", - "value": "# Upvotes 👍", - "editable": true + "value": "# Upvotes 👍" }, { "kind": 1, "language": "markdown", - "value": "## Enhancements 💪", - "editable": true + "value": "## Enhancements 💪" }, { "kind": 2, "language": "github-issues", - "value": "$pvsc $not_DS $open sort:reactions-+1-desc label:\"type-enhancement\" ", - "editable": true + "value": "$pvsc $open $upvotes label:\"feature-request\" " }, { "kind": 1, "language": "markdown", - "value": "## Bugs 🐜", - "editable": true + "value": "## Bugs 🐜" }, { "kind": 2, "language": "github-issues", - "value": "$pvsc $not_DS $open sort:reactions-+1-desc label:\"type-bug\"", - "editable": true + "value": "$pvsc $open $upvotes label:\"bug\"" } -] \ No newline at end of file +] diff --git a/src/client/activation/aaTesting.ts b/src/client/activation/aaTesting.ts deleted file mode 100644 index 1a424e5f2606..000000000000 --- a/src/client/activation/aaTesting.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { ValidateABTesting } from '../common/experiments/groups'; -import { IExperimentsManager } from '../common/types'; -import { IExtensionSingleActivationService } from './types'; - -@injectable() -export class AATesting implements IExtensionSingleActivationService { - constructor(@inject(IExperimentsManager) private experiments: IExperimentsManager) {} - - public async activate(): Promise { - this.experiments.sendTelemetryIfInExperiment(ValidateABTesting.experiment); - this.experiments.sendTelemetryIfInExperiment(ValidateABTesting.control); - } -} diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index bf53a1d18103..9e97c5c48857 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -7,40 +7,59 @@ import { inject, injectable, multiInject } from 'inversify'; import { TextDocument } from 'vscode'; import { IApplicationDiagnostics } from '../application/types'; import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { DEFAULT_INTERPRETER_SETTING, PYTHON_LANGUAGE } from '../common/constants'; -import { DeprecatePythonPath } from '../common/experiments/groups'; -import { traceDecorators } from '../common/logger'; +import { PYTHON_LANGUAGE } from '../common/constants'; import { IFileSystem } from '../common/platform/types'; -import { IDisposable, IExperimentsManager, IInterpreterPathService, Resource } from '../common/types'; -import { createDeferred, Deferred } from '../common/utils/async'; -import { IInterpreterAutoSelectionService, IInterpreterSecurityService } from '../interpreter/autoSelection/types'; -import { IInterpreterService } from '../interpreter/contracts'; +import { IDisposable, IInterpreterPathService, Resource } from '../common/types'; +import { Deferred } from '../common/utils/async'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IInterpreterAutoSelectionService } from '../interpreter/autoSelection/types'; +import { traceDecoratorError } from '../logging'; import { sendActivationTelemetry } from '../telemetry/envFileTelemetry'; import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService } from './types'; @injectable() export class ExtensionActivationManager implements IExtensionActivationManager { public readonly activatedWorkspaces = new Set(); + protected readonly isInterpreterSetForWorkspacePromises = new Map>(); + private readonly disposables: IDisposable[] = []; + private docOpenedHandler?: IDisposable; + constructor( - @multiInject(IExtensionActivationService) private readonly activationServices: IExtensionActivationService[], + @multiInject(IExtensionActivationService) private activationServices: IExtensionActivationService[], @multiInject(IExtensionSingleActivationService) - private readonly singleActivationServices: IExtensionSingleActivationService[], + private singleActivationServices: IExtensionSingleActivationService[], @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IInterpreterAutoSelectionService) private readonly autoSelection: IInterpreterAutoSelectionService, @inject(IApplicationDiagnostics) private readonly appDiagnostics: IApplicationDiagnostics, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IFileSystem) private readonly fileSystem: IFileSystem, @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, - @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, - @inject(IInterpreterSecurityService) private readonly interpreterSecurityService: IInterpreterSecurityService ) {} - public dispose() { + private filterServices() { + if (!this.workspaceService.isTrusted) { + this.activationServices = this.activationServices.filter( + (service) => service.supportedWorkspaceTypes.untrustedWorkspace, + ); + this.singleActivationServices = this.singleActivationServices.filter( + (service) => service.supportedWorkspaceTypes.untrustedWorkspace, + ); + } + if (this.workspaceService.isVirtualWorkspace) { + this.activationServices = this.activationServices.filter( + (service) => service.supportedWorkspaceTypes.virtualWorkspace, + ); + this.singleActivationServices = this.singleActivationServices.filter( + (service) => service.supportedWorkspaceTypes.virtualWorkspace, + ); + } + } + + public dispose(): void { while (this.disposables.length > 0) { const disposable = this.disposables.shift()!; disposable.dispose(); @@ -50,91 +69,65 @@ export class ExtensionActivationManager implements IExtensionActivationManager { this.docOpenedHandler = undefined; } } - public async activate(): Promise { + + public async activate(startupStopWatch: StopWatch): Promise { + this.filterServices(); await this.initialize(); + // Activate all activation services together. + await Promise.all([ - Promise.all(this.singleActivationServices.map((item) => item.activate())), - this.activateWorkspace(this.activeResourceService.getActiveResource()) + ...this.singleActivationServices.map((item) => item.activate()), + this.activateWorkspace(this.activeResourceService.getActiveResource(), startupStopWatch), ]); - await this.autoSelection.autoSelectInterpreter(undefined); } - @traceDecorators.error('Failed to activate a workspace') - public async activateWorkspace(resource: Resource) { + + @traceDecoratorError('Failed to activate a workspace') + public async activateWorkspace(resource: Resource, startupStopWatch?: StopWatch): Promise { + const folder = this.workspaceService.getWorkspaceFolder(resource); + resource = folder ? folder.uri : undefined; const key = this.getWorkspaceKey(resource); if (this.activatedWorkspaces.has(key)) { return; } this.activatedWorkspaces.add(key); - if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) { + if (this.workspaceService.isTrusted) { + // Do not interact with interpreters in a untrusted workspace. + await this.autoSelection.autoSelectInterpreter(resource); await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource); } - this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - - // Get latest interpreter list in the background. - this.interpreterService.getInterpreters(resource).ignoreErrors(); - await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource); - - await this.autoSelection.autoSelectInterpreter(resource); - await this.evaluateAutoSelectedInterpreterSafety(resource); - await Promise.all(this.activationServices.map((item) => item.activate(resource))); + await Promise.all(this.activationServices.map((item) => item.activate(resource, startupStopWatch))); await this.appDiagnostics.performPreStartupHealthCheck(resource); } - public async initialize() { + + public async initialize(): Promise { this.addHandlers(); this.addRemoveDocOpenedHandlers(); } - public onDocOpened(doc: TextDocument) { + + public onDocOpened(doc: TextDocument): void { if (doc.languageId !== PYTHON_LANGUAGE) { return; } const key = this.getWorkspaceKey(doc.uri); + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; // If we have opened a doc that does not belong to workspace, then do nothing. - if (key === '' && this.workspaceService.hasWorkspaceFolders) { + if (key === '' && hasWorkspaceFolders) { return; } if (this.activatedWorkspaces.has(key)) { return; } - const folder = this.workspaceService.getWorkspaceFolder(doc.uri); - this.activateWorkspace(folder ? folder.uri : undefined).ignoreErrors(); + this.activateWorkspace(doc.uri).ignoreErrors(); } - public async evaluateAutoSelectedInterpreterSafety(resource: Resource) { - if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) { - const workspaceKey = this.getWorkspaceKey(resource); - const interpreterSettingValue = this.interpreterPathService.get(resource); - if (interpreterSettingValue.length === 0 || interpreterSettingValue === DEFAULT_INTERPRETER_SETTING) { - // Setting is not set, extension will use the autoselected value. Make sure it's safe. - const interpreter = this.autoSelection.getAutoSelectedInterpreter(resource); - if (interpreter) { - const isInterpreterSetForWorkspace = createDeferred(); - this.isInterpreterSetForWorkspacePromises.set(workspaceKey, isInterpreterSetForWorkspace); - await Promise.race([ - isInterpreterSetForWorkspace.promise, - this.interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter, resource) - ]); - } - } else { - // Resolve any concurrent calls waiting on the promise - if (this.isInterpreterSetForWorkspacePromises.has(workspaceKey)) { - this.isInterpreterSetForWorkspacePromises.get(workspaceKey)!.resolve(); - this.isInterpreterSetForWorkspacePromises.delete(workspaceKey); - } - } - } - this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - } - - protected addHandlers() { + protected addHandlers(): void { this.disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); - this.disposables.push( - this.interpreterPathService.onDidChange((i) => this.evaluateAutoSelectedInterpreterSafety(i.uri)) - ); } - protected addRemoveDocOpenedHandlers() { + + protected addRemoveDocOpenedHandlers(): void { if (this.hasMultipleWorkspaces()) { if (!this.docOpenedHandler) { this.docOpenedHandler = this.documentManager.onDidOpenTextDocument(this.onDocOpened, this); @@ -146,10 +139,11 @@ export class ExtensionActivationManager implements IExtensionActivationManager { this.docOpenedHandler = undefined; } } - protected onWorkspaceFoldersChanged() { - //If an activated workspace folder was removed, delete its key + + protected onWorkspaceFoldersChanged(): void { + // If an activated workspace folder was removed, delete its key const workspaceKeys = this.workspaceService.workspaceFolders!.map((workspaceFolder) => - this.getWorkspaceKey(workspaceFolder.uri) + this.getWorkspaceKey(workspaceFolder.uri), ); const activatedWkspcKeys = Array.from(this.activatedWorkspaces.keys()); const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); @@ -160,10 +154,12 @@ export class ExtensionActivationManager implements IExtensionActivationManager { } this.addRemoveDocOpenedHandlers(); } - protected hasMultipleWorkspaces() { - return this.workspaceService.hasWorkspaceFolders && this.workspaceService.workspaceFolders!.length > 1; + + protected hasMultipleWorkspaces(): boolean { + return (this.workspaceService.workspaceFolders?.length || 0) > 1; } - protected getWorkspaceKey(resource: Resource) { + + protected getWorkspaceKey(resource: Resource): string { return this.workspaceService.getWorkspaceFolderIdentifier(resource, ''); } } diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts deleted file mode 100644 index f72cca3bf4b6..000000000000 --- a/src/client/activation/activationService.ts +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import '../common/extensions'; - -import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Disposable, OutputChannel, Uri } from 'vscode'; - -import { LSNotSupportedDiagnosticServiceId } from '../application/diagnostics/checks/lsNotSupported'; -import { IDiagnosticsService } from '../application/diagnostics/types'; -import { - IApplicationEnvironment, - IApplicationShell, - ICommandManager, - IWorkspaceService -} from '../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import { traceError } from '../common/logger'; -import { - IConfigurationService, - IDisposableRegistry, - IExtensions, - IOutputChannel, - IPersistentStateFactory, - IPythonSettings, - Resource -} from '../common/types'; -import { swallowExceptions } from '../common/utils/decorators'; -import { LanguageService } from '../common/utils/localize'; -import { noop } from '../common/utils/misc'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { PythonInterpreter } from '../pythonEnvironments/info'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { Commands } from './commands'; -import { LanguageServerChangeHandler } from './common/languageServerChangeHandler'; -import { RefCountedLanguageServer } from './refCountedLanguageServer'; -import { - IExtensionActivationService, - ILanguageServerActivator, - ILanguageServerCache, - LanguageServerType -} from './types'; - -const languageServerSetting: keyof IPythonSettings = 'languageServer'; -const workspacePathNameForGlobalWorkspaces = ''; - -interface IActivatedServer { - key: string; - server: ILanguageServerActivator; - jedi: boolean; -} - -@injectable() -export class LanguageServerExtensionActivationService - implements IExtensionActivationService, ILanguageServerCache, Disposable { - private cache = new Map>(); - private activatedServer?: IActivatedServer; - private readonly workspaceService: IWorkspaceService; - private readonly output: OutputChannel; - private readonly interpreterService: IInterpreterService; - private readonly languageServerChangeHandler: LanguageServerChangeHandler; - private resource!: Resource; - - constructor( - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory - ) { - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - this.interpreterService = this.serviceContainer.get(IInterpreterService); - this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - - const commandManager = this.serviceContainer.get(ICommandManager); - const disposables = serviceContainer.get(IDisposableRegistry); - disposables.push(this); - disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); - disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); - disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); - disposables.push( - commandManager.registerCommand(Commands.ClearAnalyisCache, this.onClearAnalysisCaches.bind(this)) - ); - - this.languageServerChangeHandler = new LanguageServerChangeHandler( - this.getCurrentLanguageServerType(), - this.serviceContainer.get(IExtensions), - this.serviceContainer.get(IApplicationShell), - this.serviceContainer.get(IApplicationEnvironment), - this.serviceContainer.get(ICommandManager) - ); - disposables.push(this.languageServerChangeHandler); - } - - public async activate(resource: Resource): Promise { - // Get a new server and dispose of the old one (might be the same one) - this.resource = resource; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const key = await this.getKey(resource, interpreter); - - // If we have an old server with a different key, then deactivate it as the - // creation of the new server may fail if this server is still connected - if (this.activatedServer && this.activatedServer.key !== key) { - this.activatedServer.server.deactivate(); - } - - // Get the new item - const result = await this.get(resource, interpreter); - - // Now we dispose. This ensures the object stays alive if it's the same object because - // we dispose after we increment the ref count. - if (this.activatedServer) { - this.activatedServer.server.dispose(); - } - - // Save our active server. - this.activatedServer = { key, server: result, jedi: result.type === LanguageServerType.Jedi }; - - // Force this server to reconnect (if disconnected) as it should be the active - // language server for all of VS code. - this.activatedServer.server.activate(); - } - - public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { - // See if we already have it or not - const key = await this.getKey(resource, interpreter); - let result: Promise | undefined = this.cache.get(key); - if (!result) { - // Create a special ref counted result so we don't dispose of the - // server too soon. - result = this.createRefCountedServer(resource, interpreter, key); - this.cache.set(key, result); - } else { - // Increment ref count if already exists. - result = result.then((r) => { - r.increment(); - return r; - }); - } - return result; - } - - public dispose() { - if (this.activatedServer) { - this.activatedServer.server.dispose(); - } - } - - @swallowExceptions('Send telemetry for language server current selection') - public async sendTelemetryForChosenLanguageServer(languageServer: LanguageServerType): Promise { - const state = this.stateFactory.createGlobalPersistentState( - 'SWITCH_LS', - undefined - ); - if (typeof state.value !== 'string') { - await state.updateValue(languageServer); - } - if (state.value !== languageServer) { - await state.updateValue(languageServer); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - switchTo: languageServer - }); - } else { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - lsStartup: languageServer - }); - } - } - - /** - * Checks if user does not have any `languageServer` setting set. - * @param resource - * @returns `true` if user is using default configuration, `false` if user has `languageServer` setting added. - */ - public isJediUsingDefaultConfiguration(resource: Resource): boolean { - const settings = this.workspaceService - .getConfiguration('python', resource) - .inspect('languageServer'); - if (!settings) { - traceError('WorkspaceConfiguration.inspect returns `undefined` for setting `python.languageServer`'); - return false; - } - return ( - settings.globalValue === undefined && - settings.workspaceValue === undefined && - settings.workspaceFolderValue === undefined - ); - } - - protected async onWorkspaceFoldersChanged() { - //If an activated workspace folder was removed, dispose its activator - const workspaceKeys = await Promise.all( - this.workspaceService.workspaceFolders!.map((workspaceFolder) => this.getKey(workspaceFolder.uri)) - ); - const activatedWkspcKeys = Array.from(this.cache.keys()); - const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); - if (activatedWkspcFoldersRemoved.length > 0) { - for (const folder of activatedWkspcFoldersRemoved) { - const server = await this.cache.get(folder); - server?.dispose(); // This should remove it from the cache if this is the last instance. - } - } - } - - private async onDidChangeInterpreter() { - // Reactivate the resource. It should destroy the old one if it's different. - return this.activate(this.resource); - } - - private getCurrentLanguageServerType(): LanguageServerType { - const configurationService = this.serviceContainer.get(IConfigurationService); - return configurationService.getSettings(this.resource).languageServer; - } - - private async createRefCountedServer( - resource: Resource, - interpreter: PythonInterpreter | undefined, - key: string - ): Promise { - let serverType = this.getCurrentLanguageServerType(); - if (serverType === LanguageServerType.Microsoft) { - const lsNotSupportedDiagnosticService = this.serviceContainer.get( - IDiagnosticsService, - LSNotSupportedDiagnosticServiceId - ); - const diagnostic = await lsNotSupportedDiagnosticService.diagnose(undefined); - lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); - if (diagnostic.length) { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { - supported: false - }); - serverType = LanguageServerType.Jedi; - } - } - - this.sendTelemetryForChosenLanguageServer(serverType).ignoreErrors(); - - await this.logStartup(serverType); - let server = this.serviceContainer.get(ILanguageServerActivator, serverType); - try { - await server.start(resource, interpreter); - } catch (ex) { - if (serverType === LanguageServerType.Jedi) { - throw ex; - } - this.output.appendLine(LanguageService.lsFailedToStart()); - serverType = LanguageServerType.Jedi; - server = this.serviceContainer.get(ILanguageServerActivator, serverType); - await server.start(resource, interpreter); - } - - // Wrap the returned server in something that ref counts it. - return new RefCountedLanguageServer(server, serverType, () => { - // When we finally remove the last ref count, remove from the cache - this.cache.delete(key); - - // Dispose of the actual server. - server.dispose(); - }); - } - - private async logStartup(serverType: LanguageServerType): Promise { - let outputLine; - switch (serverType) { - case LanguageServerType.Jedi: - outputLine = LanguageService.startingJedi(); - break; - case LanguageServerType.Microsoft: - outputLine = LanguageService.startingMicrosoft(); - break; - case LanguageServerType.Node: - outputLine = LanguageService.startingPylance(); - break; - case LanguageServerType.None: - outputLine = LanguageService.startingNone(); - break; - default: - throw new Error('Unknown langauge server type in activator.'); - } - this.output.appendLine(outputLine); - } - - private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { - const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders - ? this.workspaceService.workspaceFolders!.map((workspace) => workspace.uri) - : [undefined]; - if ( - workspacesUris.findIndex((uri) => event.affectsConfiguration(`python.${languageServerSetting}`, uri)) === -1 - ) { - return; - } - const lsType = this.getCurrentLanguageServerType(); - if (this.activatedServer?.key !== lsType) { - await this.languageServerChangeHandler.handleLanguageServerChange(lsType); - } - } - - private async getKey(resource: Resource, interpreter?: PythonInterpreter): Promise { - const configurationService = this.serviceContainer.get(IConfigurationService); - const serverType = configurationService.getSettings(this.resource).languageServer; - if (serverType === LanguageServerType.Node) { - return 'shared-ls'; - } - - const resourcePortion = this.workspaceService.getWorkspaceFolderIdentifier( - resource, - workspacePathNameForGlobalWorkspaces - ); - interpreter = interpreter ? interpreter : await this.interpreterService.getActiveInterpreter(resource); - const interperterPortion = interpreter ? `${interpreter.path}-${interpreter.envName}` : ''; - return `${resourcePortion}-${interperterPortion}`; - } - - private async onClearAnalysisCaches() { - const values = await Promise.all([...this.cache.values()]); - values.forEach((v) => (v.clearAnalysisCache ? v.clearAnalysisCache() : noop())); - } -} diff --git a/src/client/activation/commands.ts b/src/client/activation/commands.ts index 6a6628e6d4a3..158d9662ec46 100644 --- a/src/client/activation/commands.ts +++ b/src/client/activation/commands.ts @@ -3,6 +3,5 @@ 'use strict'; export namespace Commands { - export const ClearAnalyisCache = 'python.analysis.clearCache'; export const RestartLS = 'python.analysis.restartLanguageServer'; } diff --git a/src/client/activation/common/activatorBase.ts b/src/client/activation/common/activatorBase.ts deleted file mode 100644 index c267d0c0bc59..000000000000 --- a/src/client/activation/common/activatorBase.ts +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - TextDocumentContentChangeEvent, - WorkspaceEdit -} from 'vscode'; -import * as vscodeLanguageClient from 'vscode-languageclient/node'; - -import { injectable } from 'inversify'; -import { IWorkspaceService } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { ILanguageServerActivator, ILanguageServerManager } from '../types'; - -/** - * Starts the language server managers per workspaces (currently one for first workspace). - * - * @export - * @class LanguageServerActivatorBase - * @implements {ILanguageServerActivator} - */ -@injectable() -export abstract class LanguageServerActivatorBase implements ILanguageServerActivator { - protected resource?: Resource; - constructor( - protected readonly manager: ILanguageServerManager, - private readonly workspace: IWorkspaceService, - protected readonly fs: IFileSystem, - protected readonly configurationService: IConfigurationService - ) {} - - @traceDecorators.error('Failed to activate language server') - public async start(resource: Resource, interpreter?: PythonInterpreter): Promise { - if (!resource) { - resource = this.workspace.hasWorkspaceFolders ? this.workspace.workspaceFolders![0].uri : undefined; - } - this.resource = resource; - await this.ensureLanguageServerIsAvailable(resource); - await this.manager.start(resource, interpreter); - } - - public dispose(): void { - this.manager.dispose(); - } - - public abstract async ensureLanguageServerIsAvailable(resource: Resource): Promise; - - public activate(): void { - this.manager.connect(); - } - - public deactivate(): void { - this.manager.disconnect(); - } - - public handleOpen(document: TextDocument): void { - const languageClient = this.getLanguageClient(); - if (languageClient) { - languageClient.sendNotification( - vscodeLanguageClient.DidOpenTextDocumentNotification.type, - languageClient.code2ProtocolConverter.asOpenTextDocumentParams(document) - ); - } - } - - public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void { - const languageClient = this.getLanguageClient(); - if (languageClient) { - // If the language client doesn't support incremental, just send the whole document - if (this.textDocumentSyncKind === vscodeLanguageClient.TextDocumentSyncKind.Full) { - languageClient.sendNotification( - vscodeLanguageClient.DidChangeTextDocumentNotification.type, - languageClient.code2ProtocolConverter.asChangeTextDocumentParams(document) - ); - } else { - languageClient.sendNotification( - vscodeLanguageClient.DidChangeTextDocumentNotification.type, - languageClient.code2ProtocolConverter.asChangeTextDocumentParams({ - document, - contentChanges: changes - }) - ); - } - } - } - - public provideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken - ): ProviderResult { - return this.handleProvideRenameEdits(document, position, newName, token); - } - - public provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken - ): ProviderResult { - return this.handleProvideDefinition(document, position, token); - } - - public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - return this.handleProvideHover(document, position, token); - } - - public provideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken - ): ProviderResult { - return this.handleProvideReferences(document, position, context, token); - } - - public provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext - ): ProviderResult { - return this.handleProvideCompletionItems(document, position, token, context); - } - - public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { - return this.handleProvideCodeLenses(document, token); - } - - public provideDocumentSymbols( - document: TextDocument, - token: CancellationToken - ): ProviderResult { - return this.handleProvideDocumentSymbols(document, token); - } - - public provideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - context: SignatureHelpContext - ): ProviderResult { - return this.handleProvideSignatureHelp(document, position, token, context); - } - - protected getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { - const proxy = this.manager.languageProxy; - if (proxy) { - return proxy.languageClient; - } - } - - private get textDocumentSyncKind(): vscodeLanguageClient.TextDocumentSyncKind { - const languageClient = this.getLanguageClient(); - if (languageClient?.initializeResult?.capabilities?.textDocumentSync) { - const syncOptions = languageClient.initializeResult.capabilities.textDocumentSync; - const syncKind = - syncOptions !== undefined && syncOptions.hasOwnProperty('change') - ? (syncOptions as vscodeLanguageClient.TextDocumentSyncOptions).change - : syncOptions; - if (syncKind !== undefined) { - return syncKind as vscodeLanguageClient.TextDocumentSyncKind; - } - } - - // Default is full if not provided - return vscodeLanguageClient.TextDocumentSyncKind.Full; - } - - private async handleProvideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.RenameParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - newName - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.RenameRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asWorkspaceEdit(result); - } - } - } - - private async handleProvideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position) - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.DefinitionRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asDefinitionResult(result); - } - } - } - - private async handleProvideHover( - document: TextDocument, - position: Position, - token: CancellationToken - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position) - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.HoverRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asHover(result); - } - } - } - - private async handleProvideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.ReferenceParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - context - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.ReferencesRequest.type, args, token); - if (result) { - // Remove undefined part. - return result.map((l) => { - const r = languageClient!.protocol2CodeConverter.asLocation(l); - return r!; - }); - } - } - } - - private async handleProvideCodeLenses( - document: TextDocument, - token: CancellationToken - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.CodeLensParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document) - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.CodeLensRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asCodeLenses(result); - } - } - } - - private async handleProvideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args = languageClient.code2ProtocolConverter.asCompletionParams(document, position, context); - const result = await languageClient.sendRequest(vscodeLanguageClient.CompletionRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asCompletionResult(result); - } - } - } - - private async handleProvideDocumentSymbols( - document: TextDocument, - token: CancellationToken - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.DocumentSymbolParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document) - }; - const result = await languageClient.sendRequest( - vscodeLanguageClient.DocumentSymbolRequest.type, - args, - token - ); - if (result && result.length) { - // tslint:disable-next-line: no-any - if ((result[0] as any).range) { - // Document symbols - const docSymbols = result as vscodeLanguageClient.DocumentSymbol[]; - return languageClient.protocol2CodeConverter.asDocumentSymbols(docSymbols); - } else { - // Document symbols - const symbols = result as vscodeLanguageClient.SymbolInformation[]; - return languageClient.protocol2CodeConverter.asSymbolInformations(symbols); - } - } - } - } - - private async handleProvideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - _context: SignatureHelpContext - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position) - }; - const result = await languageClient.sendRequest( - vscodeLanguageClient.SignatureHelpRequest.type, - args, - token - ); - if (result) { - return languageClient.protocol2CodeConverter.asSignatureHelp(result); - } - } - } -} diff --git a/src/client/activation/common/analysisOptions.ts b/src/client/activation/common/analysisOptions.ts index 48f03923ba3e..75d0aabef9d2 100644 --- a/src/client/activation/common/analysisOptions.ts +++ b/src/client/activation/common/analysisOptions.ts @@ -1,46 +1,39 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable } from 'inversify'; import { Disposable, Event, EventEmitter, WorkspaceFolder } from 'vscode'; import { DocumentFilter, LanguageClientOptions, RevealOutputChannelOn } from 'vscode-languageclient/node'; +import { IWorkspaceService } from '../../common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../common/constants'; -import { traceDecorators } from '../../common/logger'; -import { IOutputChannel, Resource } from '../../common/types'; +import { ILogOutputChannel, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { traceDecoratorError } from '../../logging'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; import { ILanguageServerAnalysisOptions, ILanguageServerOutputChannel } from '../types'; -@injectable() export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServerAnalysisOptions { - protected disposables: Disposable[] = []; protected readonly didChange = new EventEmitter(); - private envPythonPath: string = ''; - private readonly output: IOutputChannel; + private readonly output: ILogOutputChannel; protected constructor( - private readonly envVarsProvider: IEnvironmentVariablesProvider, - lsOutputChannel: ILanguageServerOutputChannel + lsOutputChannel: ILanguageServerOutputChannel, + protected readonly workspace: IWorkspaceService, ) { this.output = lsOutputChannel.channel; } - public async initialize(_resource: Resource, _interpreter: PythonInterpreter | undefined) { - const disposable = this.envVarsProvider.onDidEnvironmentVariablesChange(this.onEnvVarChange, this); - this.disposables.push(disposable); - } + public async initialize(_resource: Resource, _interpreter: PythonEnvironment | undefined) {} public get onDidChange(): Event { return this.didChange.event; } public dispose(): void { - this.disposables.forEach((d) => d.dispose()); this.didChange.dispose(); } - @traceDecorators.error('Failed to get analysis options') + @traceDecoratorError('Failed to get analysis options') public async getAnalysisOptions(): Promise { const workspaceFolder = this.getWorkspaceFolder(); const documentSelector = this.getDocumentFilters(workspaceFolder); @@ -49,11 +42,11 @@ export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServ documentSelector, workspaceFolder, synchronize: { - configurationSection: PYTHON_LANGUAGE + configurationSection: this.getConfigSectionsToSynchronize(), }, outputChannel: this.output, revealOutputChannelOn: RevealOutputChannelOn.Never, - initializationOptions: await this.getInitializationOptions() + initializationOptions: await this.getInitializationOptions(), }; } @@ -62,13 +55,39 @@ export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServ } protected getDocumentFilters(_workspaceFolder?: WorkspaceFolder): DocumentFilter[] { - return PYTHON; + return this.workspace.isVirtualWorkspace ? [{ language: PYTHON_LANGUAGE }] : PYTHON; + } + + protected getConfigSectionsToSynchronize(): string[] { + return [PYTHON_LANGUAGE]; } - // tslint:disable-next-line: no-any protected async getInitializationOptions(): Promise { return undefined; } +} + +export abstract class LanguageServerAnalysisOptionsWithEnv extends LanguageServerAnalysisOptionsBase { + protected disposables: Disposable[] = []; + private envPythonPath: string = ''; + + protected constructor( + private readonly envVarsProvider: IEnvironmentVariablesProvider, + lsOutputChannel: ILanguageServerOutputChannel, + workspace: IWorkspaceService, + ) { + super(lsOutputChannel, workspace); + } + + public async initialize(_resource: Resource, _interpreter: PythonEnvironment | undefined) { + const disposable = this.envVarsProvider.onDidEnvironmentVariablesChange(this.onEnvVarChange, this); + this.disposables.push(disposable); + } + + public dispose(): void { + super.dispose(); + this.disposables.forEach((d) => d.dispose()); + } protected async getEnvPythonPath(): Promise { const vars = await this.envVarsProvider.getEnvironmentVariables(); diff --git a/src/client/activation/node/cancellationUtils.ts b/src/client/activation/common/cancellationUtils.ts similarity index 92% rename from src/client/activation/node/cancellationUtils.ts rename to src/client/activation/common/cancellationUtils.ts index 18cf55705765..d14307174107 100644 --- a/src/client/activation/node/cancellationUtils.ts +++ b/src/client/activation/common/cancellationUtils.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ /* * cancellationUtils.ts * Copyright (c) Microsoft Corporation. @@ -15,7 +16,7 @@ import { CancellationSenderStrategy, CancellationStrategy, Disposable, - MessageConnection + MessageConnection, } from 'vscode-languageclient/node'; type CancellationId = string | number; @@ -32,7 +33,7 @@ function tryRun(callback: () => void) { try { callback(); } catch (e) { - // tslint:disable-next-line: no-empty + // No body. } } @@ -42,7 +43,7 @@ class FileCancellationSenderStrategy implements CancellationSenderStrategy { tryRun(() => fs.mkdirSync(folder, { recursive: true })); } - public sendCancellation(_: MessageConnection, id: CancellationId): void { + public async sendCancellation(_: MessageConnection, id: CancellationId) { const file = getCancellationFilePath(this.folderName, id); tryRun(() => fs.writeFileSync(file, '', { flag: 'w' })); } @@ -80,6 +81,7 @@ export class FileBasedCancellationStrategy implements CancellationStrategy, Disp this._sender = new FileCancellationSenderStrategy(folderName); } + // eslint-disable-next-line class-methods-use-this get receiver(): CancellationReceiverStrategy { return CancellationReceiverStrategy.Message; } diff --git a/src/client/activation/common/defaultlanguageServer.ts b/src/client/activation/common/defaultlanguageServer.ts new file mode 100644 index 000000000000..dc40a2c0ed5b --- /dev/null +++ b/src/client/activation/common/defaultlanguageServer.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { IDefaultLanguageServer, IExtensions, DefaultLSType } from '../../common/types'; +import { IServiceManager } from '../../ioc/types'; +import { LanguageServerType } from '../types'; + +@injectable() +class DefaultLanguageServer implements IDefaultLanguageServer { + public readonly defaultLSType: DefaultLSType; + + constructor(defaultServer: DefaultLSType) { + this.defaultLSType = defaultServer; + } +} + +export async function setDefaultLanguageServer( + extensions: IExtensions, + serviceManager: IServiceManager, +): Promise { + const lsType = await getDefaultLanguageServer(extensions); + serviceManager.addSingletonInstance( + IDefaultLanguageServer, + new DefaultLanguageServer(lsType), + ); +} + +async function getDefaultLanguageServer(extensions: IExtensions): Promise { + if (extensions.getExtension(PYLANCE_EXTENSION_ID)) { + return LanguageServerType.Node; + } + + return LanguageServerType.Jedi; +} diff --git a/src/client/activation/common/downloadChannelRules.ts b/src/client/activation/common/downloadChannelRules.ts deleted file mode 100644 index ebffee922377..000000000000 --- a/src/client/activation/common/downloadChannelRules.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IPersistentStateFactory } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { FolderVersionPair, IDownloadChannelRule } from '../types'; - -const lastCheckedForLSDateTimeCacheKey = 'LS.LAST.CHECK.TIME'; -const frequencyForBetalLSDownloadCheck = 1000 * 60 * 60 * 24; // One day. - -@injectable() -export class DownloadDailyChannelRule implements IDownloadChannelRule { - public async shouldLookForNewLanguageServer(_currentFolder?: FolderVersionPair): Promise { - return true; - } -} -@injectable() -export class DownloadStableChannelRule implements IDownloadChannelRule { - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise { - return currentFolder ? false : true; - } -} -@injectable() -export class DownloadBetaChannelRule implements IDownloadChannelRule { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise { - // For beta, we do this only once a day. - const stateFactory = this.serviceContainer.get(IPersistentStateFactory); - const globalState = stateFactory.createGlobalPersistentState( - lastCheckedForLSDateTimeCacheKey, - true, - frequencyForBetalLSDownloadCheck - ); - - // If we have checked it in the last 24 hours, then ensure we don't do it again. - if (globalState.value) { - await globalState.updateValue(false); - return true; - } - - return !currentFolder || globalState.value; - } -} diff --git a/src/client/activation/common/downloader.ts b/src/client/activation/common/downloader.ts deleted file mode 100644 index b4e690e6b919..000000000000 --- a/src/client/activation/common/downloader.ts +++ /dev/null @@ -1,202 +0,0 @@ -// 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 { ProgressLocation, window } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; -import '../../common/extensions'; -import { traceError } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; -import { IFileDownloader, IOutputChannel, Resource } from '../../common/types'; -import { createDeferred } from '../../common/utils/async'; -import { Common, LanguageService } from '../../common/utils/localize'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { - ILanguageServerDownloader, - ILanguageServerFolderService, - ILanguageServerOutputChannel, - IPlatformData -} from '../types'; - -// tslint:disable:no-require-imports no-any - -const downloadFileExtension = '.nupkg'; - -@injectable() -export class LanguageServerDownloader implements ILanguageServerDownloader { - private output: IOutputChannel; - constructor( - @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, - @inject(IFileDownloader) private readonly fileDownloader: IFileDownloader, - @inject(ILanguageServerFolderService) private readonly lsFolderService: ILanguageServerFolderService, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IServiceContainer) private readonly services: IServiceContainer - ) { - this.output = this.lsOutputChannel.channel; - } - - public async getDownloadInfo(resource: Resource) { - const info = await this.lsFolderService.getLatestLanguageServerVersion(resource).then((item) => item!); - - let uri = info.uri; - if (uri.startsWith('https:')) { - const cfg = this.workspace.getConfiguration('http', resource); - if (!cfg.get('proxyStrictSSL', true)) { - // tslint:disable-next-line:no-http-string - uri = uri.replace(/^https:/, 'http:'); - } - } - const lsNameTrimmed = info.package.split('.')[0]; - return [uri, info.version.raw, lsNameTrimmed]; - } - - public async downloadLanguageServer(destinationFolder: string, resource: Resource): Promise { - if (await this.lsFolderService.skipDownload()) { - // Sanity check; this case should not be hit if skipDownload is true elsewhere. - traceError('Attempted to download with skipDownload true.'); - return; - } - - const [downloadUri, lsVersion, lsName] = await this.getDownloadInfo(resource); - const timer: StopWatch = new StopWatch(); - let success: boolean = true; - let localTempFilePath = ''; - - try { - localTempFilePath = await this.downloadFile( - downloadUri, - 'Downloading Microsoft Python Language Server... ' - ); - } catch (err) { - this.output.appendLine(LanguageService.downloadFailedOutputMessage()); - this.output.appendLine(err); - success = false; - this.showMessageAndOptionallyShowOutput(LanguageService.lsFailedToDownload()).ignoreErrors(); - sendTelemetryEvent( - EventName.PYTHON_LANGUAGE_SERVER_ERROR, - undefined, - { error: 'Failed to download (platform)' }, - err - ); - throw new Error(err); - } finally { - const usedSSL = downloadUri.startsWith('https:'); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_DOWNLOADED, timer.elapsedTime, { - success, - lsVersion, - usedSSL, - lsName - }); - } - - timer.reset(); - try { - await this.unpackArchive(destinationFolder, localTempFilePath); - } catch (err) { - this.output.appendLine(LanguageService.extractionFailedOutputMessage()); - this.output.appendLine(err); - success = false; - this.showMessageAndOptionallyShowOutput(LanguageService.lsFailedToExtract()).ignoreErrors(); - sendTelemetryEvent( - EventName.PYTHON_LANGUAGE_SERVER_ERROR, - undefined, - { error: 'Failed to extract (platform)' }, - err - ); - throw new Error(err); - } finally { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_EXTRACTED, timer.elapsedTime, { - success, - lsVersion, - lsName - }); - await this.fs.deleteFile(localTempFilePath); - } - } - - public async showMessageAndOptionallyShowOutput(message: string) { - const selection = await this.appShell.showErrorMessage(message, Common.openOutputPanel()); - if (selection !== Common.openOutputPanel()) { - return; - } - this.output.show(true); - } - - public async downloadFile(uri: string, title: string): Promise { - const downloadOptions = { - extension: downloadFileExtension, - outputChannel: this.output, - progressMessagePrefix: title - }; - return this.fileDownloader.downloadFile(uri, downloadOptions).then((file) => { - this.output.appendLine(LanguageService.extractionCompletedOutputMessage()); - return file; - }); - } - - protected async unpackArchive(destinationFolder: string, tempFilePath: string): Promise { - this.output.append('Unpacking archive... '); - - const deferred = createDeferred(); - - const title = 'Extracting files... '; - await window.withProgress( - { - location: ProgressLocation.Window - }, - (progress) => { - // tslint:disable-next-line:no-require-imports no-var-requires - const StreamZip = require('node-stream-zip'); - const zip = new StreamZip({ - file: tempFilePath, - storeEntries: true - }); - - let totalFiles = 0; - let extractedFiles = 0; - zip.on('ready', async () => { - totalFiles = zip.entriesCount; - if (!(await this.fs.directoryExists(destinationFolder))) { - await this.fs.createDirectory(destinationFolder); - } - zip.extract(null, destinationFolder, (err: any) => { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(); - } - zip.close(); - }); - }) - .on('extract', () => { - extractedFiles += 1; - progress.report({ message: `${title}${Math.round((100 * extractedFiles) / totalFiles)}%` }); - }) - .on('error', (e: any) => { - deferred.reject(e); - }); - return deferred.promise; - } - ); - - // Set file to executable (nothing happens in Windows, as chmod has no definition there) - if (this.services) { - try { - const platformData = this.services.get(IPlatformData); - const executablePath = path.join(destinationFolder, platformData.engineExecutableName); - await this.fs.chmod(executablePath, '0764'); // -rwxrw-r-- - // tslint:disable-next-line: no-empty - } catch {} - } - - this.output.appendLine(LanguageService.extractionDoneOutputMessage()); - } -} diff --git a/src/client/activation/common/languageServerChangeHandler.ts b/src/client/activation/common/languageServerChangeHandler.ts index c154ba83b1fb..83ff204ed6e7 100644 --- a/src/client/activation/common/languageServerChangeHandler.ts +++ b/src/client/activation/common/languageServerChangeHandler.ts @@ -1,28 +1,42 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Disposable } from 'vscode'; -import { IApplicationEnvironment, IApplicationShell, ICommandManager } from '../../common/application/types'; +import { ConfigurationTarget, Disposable } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { IExtensions } from '../../common/types'; +import { IConfigurationService, IExtensions } from '../../common/types'; import { createDeferred } from '../../common/utils/async'; -import { Common, LanguageService, Pylance } from '../../common/utils/localize'; -import { getPylanceExtensionUri } from '../../languageServices/proposeLanguageServerBanner'; +import { Pylance } from '../../common/utils/localize'; import { LanguageServerType } from '../types'; export async function promptForPylanceInstall( appShell: IApplicationShell, - appEnv: IApplicationEnvironment + commandManager: ICommandManager, + workspace: IWorkspaceService, + configService: IConfigurationService, ): Promise { - // If not installed, point user to Pylance at the store. const response = await appShell.showWarningMessage( - Pylance.installPylanceMessage(), - Common.bannerLabelYes(), - Common.bannerLabelNo() + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, ); - if (response === Common.bannerLabelYes()) { - appShell.openUrl(getPylanceExtensionUri(appEnv)); + if (response === Pylance.pylanceInstallPylance) { + commandManager.executeCommand('extension.open', PYLANCE_EXTENSION_ID); + } else if (response === Pylance.pylanceRevertToJedi) { + const inspection = workspace.getConfiguration('python').inspect('languageServer'); + + let target: ConfigurationTarget | undefined; + if (inspection?.workspaceValue) { + target = ConfigurationTarget.Workspace; + } else if (inspection?.globalValue) { + target = ConfigurationTarget.Global; + } + + if (target) { + await configService.updateSetting('languageServer', LanguageServerType.Jedi, undefined, target); + } } } @@ -30,21 +44,24 @@ export async function promptForPylanceInstall( export class LanguageServerChangeHandler implements Disposable { // For tests that need to track Pylance install completion. private readonly pylanceInstallCompletedDeferred = createDeferred(); + private readonly disposables: Disposable[] = []; + private pylanceInstalled = false; constructor( private currentLsType: LanguageServerType | undefined, private readonly extensions: IExtensions, private readonly appShell: IApplicationShell, - private readonly appEnv: IApplicationEnvironment, - private readonly commands: ICommandManager + private readonly commands: ICommandManager, + private readonly workspace: IWorkspaceService, + private readonly configService: IConfigurationService, ) { this.pylanceInstalled = this.isPylanceInstalled(); this.disposables.push( extensions.onDidChange(async () => { await this.extensionsChangeHandler(); - }) + }), ); } @@ -60,7 +77,7 @@ export class LanguageServerChangeHandler implements Disposable { } public async handleLanguageServerChange(lsType: LanguageServerType | undefined): Promise { - if (this.currentLsType === lsType) { + if (this.currentLsType === lsType || lsType === LanguageServerType.Microsoft) { return; } // VS Code has to be reloaded when language server type changes. In case of Pylance @@ -69,42 +86,23 @@ export class LanguageServerChangeHandler implements Disposable { // may get one reload prompt now and then another when Pylance is finally installed. // Instead, check the installation and suppress prompt if Pylance is not there. // Extensions change event handler will then show its own prompt. - let response: string | undefined; if (lsType === LanguageServerType.Node && !this.isPylanceInstalled()) { // If not installed, point user to Pylance at the store. - await promptForPylanceInstall(this.appShell, this.appEnv); + await promptForPylanceInstall(this.appShell, this.commands, this.workspace, this.configService); // At this point Pylance is not yet installed. Skip reload prompt // since we are going to show it when Pylance becomes available. - } else { - response = await this.appShell.showInformationMessage( - LanguageService.reloadAfterLanguageServerChange(), - Common.reload() - ); - if (response === Common.reload()) { - this.commands.executeCommand('workbench.action.reloadWindow'); - } } + this.currentLsType = lsType; } private async extensionsChangeHandler(): Promise { // Track Pylance extension installation state and prompt to reload when it becomes available. const oldInstallState = this.pylanceInstalled; + this.pylanceInstalled = this.isPylanceInstalled(); if (oldInstallState === this.pylanceInstalled) { this.pylanceInstallCompletedDeferred.resolve(); - return; - } - - const response = await this.appShell.showWarningMessage( - Pylance.pylanceInstalledReloadPromptMessage(), - Common.bannerLabelYes(), - Common.bannerLabelNo() - ); - - this.pylanceInstallCompletedDeferred.resolve(); - if (response === Common.bannerLabelYes()) { - this.commands.executeCommand('workbench.action.reloadWindow'); } } diff --git a/src/client/activation/common/languageServerFolderService.ts b/src/client/activation/common/languageServerFolderService.ts deleted file mode 100644 index ae501d8b709e..000000000000 --- a/src/client/activation/common/languageServerFolderService.ts +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, unmanaged } from 'inversify'; -import * as path from 'path'; -import * as semver from 'semver'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { traceDecorators } from '../../common/logger'; -import { NugetPackage } from '../../common/nuget/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, Resource } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { - FolderVersionPair, - IDownloadChannelRule, - ILanguageServerFolderService, - ILanguageServerPackageService -} from '../types'; - -@injectable() -export abstract class LanguageServerFolderService implements ILanguageServerFolderService { - constructor( - @inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer, - @unmanaged() protected readonly languageServerFolder: string - ) {} - - public async skipDownload(): Promise { - return false; - } - - @traceDecorators.verbose('Get language server folder name') - public async getLanguageServerFolderName(resource: Resource): Promise { - const currentFolder = await this.getCurrentLanguageServerDirectory(); - let serverVersion: NugetPackage | undefined; - - const shouldLookForNewVersion = await this.shouldLookForNewLanguageServer(currentFolder); - if (currentFolder && !shouldLookForNewVersion) { - return path.basename(currentFolder.path); - } - - try { - serverVersion = await this.getLatestLanguageServerVersion(resource); - } catch { - serverVersion = undefined; - } - - if (currentFolder && (!serverVersion || serverVersion.version.compare(currentFolder.version) <= 0)) { - return path.basename(currentFolder.path); - } - - return `${this.languageServerFolder}.${serverVersion!.version.raw}`; - } - - @traceDecorators.verbose('Get latest version of language server') - public getLatestLanguageServerVersion(resource: Resource): Promise { - const minVersion = this.getMinimalLanguageServerVersion(); - const lsPackageService = this.serviceContainer.get( - ILanguageServerPackageService - ); - return lsPackageService.getLatestNugetPackageVersion(resource, minVersion); - } - - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise { - const configService = this.serviceContainer.get(IConfigurationService); - const autoUpdateLanguageServer = configService.getSettings().autoUpdateLanguageServer; - const downloadLanguageServer = configService.getSettings().downloadLanguageServer; - if (currentFolder && (!autoUpdateLanguageServer || !downloadLanguageServer)) { - return false; - } - const downloadChannel = this.getDownloadChannel(); - const rule = this.serviceContainer.get(IDownloadChannelRule, downloadChannel); - return rule.shouldLookForNewLanguageServer(currentFolder); - } - - public async getCurrentLanguageServerDirectory(): Promise { - const configService = this.serviceContainer.get(IConfigurationService); - if (!configService.getSettings().downloadLanguageServer) { - return { path: this.languageServerFolder, version: new semver.SemVer('0.0.0') }; - } - const dirs = await this.getExistingLanguageServerDirectories(); - if (dirs.length === 0) { - return; - } - dirs.sort((a, b) => a.version.compare(b.version)); - return dirs[dirs.length - 1]; - } - - public async getExistingLanguageServerDirectories(): Promise { - const fs = this.serviceContainer.get(IFileSystem); - const subDirs = await fs.getSubDirectories(EXTENSION_ROOT_DIR); - return subDirs - .filter((dir) => path.basename(dir).startsWith(this.languageServerFolder)) - .map((dir) => { - return { path: dir, version: this.getFolderVersion(path.basename(dir)) }; - }); - } - - public getFolderVersion(dirName: string): semver.SemVer { - const suffix = dirName.substring(this.languageServerFolder.length + 1); - return suffix.length === 0 - ? new semver.SemVer('0.0.0') - : semver.parse(suffix, true) || new semver.SemVer('0.0.0'); - } - - protected abstract getMinimalLanguageServerVersion(): string; - - private getDownloadChannel() { - const lsPackageService = this.serviceContainer.get( - ILanguageServerPackageService - ); - return lsPackageService.getLanguageServerDownloadChannel(); - } -} diff --git a/src/client/activation/common/languageServerPackageService.ts b/src/client/activation/common/languageServerPackageService.ts deleted file mode 100644 index feed13c03b53..000000000000 --- a/src/client/activation/common/languageServerPackageService.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import { parse, SemVer } from 'semver'; -import { IApplicationEnvironment } from '../../common/application/types'; -import { PVSC_EXTENSION_ID } from '../../common/constants'; -import { traceDecorators, traceVerbose } from '../../common/logger'; -import { INugetRepository, INugetService, NugetPackage } from '../../common/nuget/types'; -import { IPlatformService } from '../../common/platform/types'; -import { IConfigurationService, IExtensions, LanguageServerDownloadChannels, Resource } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { ILanguageServerPackageService } from '../types'; -import { azureCDNBlobStorageAccount, LanguageServerDownloadChannel } from './packageRepository'; - -export const maxMajorVersion = 0; - -@injectable() -export abstract class LanguageServerPackageService implements ILanguageServerPackageService { - public maxMajorVersion: number = maxMajorVersion; - constructor( - protected readonly serviceContainer: IServiceContainer, - protected readonly appEnv: IApplicationEnvironment, - protected readonly platform: IPlatformService - ) {} - - public abstract getNugetPackageName(): string; - - @traceDecorators.verbose('Get latest language server nuget package version') - public async getLatestNugetPackageVersion(resource: Resource, minVersion?: string): Promise { - const downloadChannel = this.getLanguageServerDownloadChannel(); - const nugetRepo = this.serviceContainer.get(INugetRepository, downloadChannel); - const packageName = this.getNugetPackageName(); - traceVerbose(`Listing packages for ${downloadChannel} for ${packageName}`); - const packages = await nugetRepo.getPackages(packageName, resource); - - return this.getValidPackage(packages, minVersion); - } - - public getLanguageServerDownloadChannel(): LanguageServerDownloadChannels { - const configService = this.serviceContainer.get(IConfigurationService); - const settings = configService.getSettings(); - if (settings.analysis.downloadChannel) { - return settings.analysis.downloadChannel; - } - - if (settings.insidersChannel === 'daily' || settings.insidersChannel === 'weekly') { - return 'beta'; - } - const isAlphaVersion = this.isAlphaVersionOfExtension(); - return isAlphaVersion ? 'beta' : 'stable'; - } - - public isAlphaVersionOfExtension() { - const extensions = this.serviceContainer.get(IExtensions); - const extension = extensions.getExtension(PVSC_EXTENSION_ID)!; - const version = parse(extension.packageJSON.version)!; - return version.prerelease.length > 0 && version.prerelease[0] === 'alpha'; - } - - protected getValidPackage(packages: NugetPackage[], minimumVersion?: string): NugetPackage { - const nugetService = this.serviceContainer.get(INugetService); - const validPackages = packages - .filter((item) => item.version.major === this.maxMajorVersion) - .filter((item) => nugetService.isReleaseVersion(item.version)) - .sort((a, b) => a.version.compare(b.version)); - - const pkg = validPackages[validPackages.length - 1]; - minimumVersion = minimumVersion || '0.0.0'; - if (pkg.version.compare(minimumVersion) >= 0) { - return validPackages[validPackages.length - 1]; - } - - // This is a fall back, if the wrong version is returned, e.g. version is cached downstream in some proxy server or similar. - // This way, we always ensure we have the minimum version that's compatible. - return { - version: new SemVer(minimumVersion), - package: LanguageServerDownloadChannel.stable, - uri: `${azureCDNBlobStorageAccount}/${ - LanguageServerDownloadChannel.stable - }/${this.getNugetPackageName()}.${minimumVersion}.nupkg` - }; - } -} diff --git a/src/client/activation/common/loadLanguageServerExtension.ts b/src/client/activation/common/loadLanguageServerExtension.ts new file mode 100644 index 000000000000..87fa5d9e6213 --- /dev/null +++ b/src/client/activation/common/loadLanguageServerExtension.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ICommandManager } from '../../common/application/types'; +import { IDisposableRegistry } from '../../common/types'; +import { IExtensionSingleActivationService } from '../types'; + +// This command is currently used by IntelliCode. This was used to +// trigger MPLS. Since we no longer have MPLS we are going to set +// this command to no-op temporarily until this is removed from +// IntelliCode + +@injectable() +export class LoadLanguageServerExtension implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public activate(): Promise { + const disposable = this.commandManager.registerCommand('python._loadLanguageServerExtension', () => { + /** no-op */ + }); + this.disposables.push(disposable); + return Promise.resolve(); + } +} diff --git a/src/client/activation/common/outputChannel.ts b/src/client/activation/common/outputChannel.ts new file mode 100644 index 000000000000..60a99687793e --- /dev/null +++ b/src/client/activation/common/outputChannel.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 { IApplicationShell, ICommandManager } from '../../common/application/types'; +import '../../common/extensions'; +import { IDisposableRegistry, ILogOutputChannel } from '../../common/types'; +import { OutputChannelNames } from '../../common/utils/localize'; +import { ILanguageServerOutputChannel } from '../types'; + +@injectable() +export class LanguageServerOutputChannel implements ILanguageServerOutputChannel { + public output: ILogOutputChannel | undefined; + + private registered = false; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposable: IDisposableRegistry, + ) {} + + public get channel(): ILogOutputChannel { + if (!this.output) { + this.output = this.appShell.createOutputChannel(OutputChannelNames.languageServer); + this.disposable.push(this.output); + this.registerCommand().ignoreErrors(); + } + return this.output; + } + + private async registerCommand() { + if (this.registered) { + return; + } + this.registered = true; + // This controls the visibility of the command used to display the LS Output panel. + // We don't want to display it when Jedi is used instead of LS. + await this.commandManager.executeCommand('setContext', 'python.hasLanguageServerOutputChannel', true); + this.disposable.push( + this.commandManager.registerCommand('python.viewLanguageServerOutput', () => this.output?.show(true)), + ); + this.disposable.push({ + dispose: () => { + this.registered = false; + }, + }); + } +} diff --git a/src/client/activation/common/packageRepository.ts b/src/client/activation/common/packageRepository.ts deleted file mode 100644 index ba073d7b1808..000000000000 --- a/src/client/activation/common/packageRepository.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import { AzureBlobStoreNugetRepository } from '../../common/nuget/azureBlobStoreNugetRepository'; -import { IServiceContainer } from '../../ioc/types'; - -export const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; -export const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; - -export enum LanguageServerDownloadChannel { - stable = 'stable', - beta = 'beta', - daily = 'daily' -} - -@injectable() -export abstract class StableLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { - constructor(serviceContainer: IServiceContainer, packageName: string) { - super( - serviceContainer, - azureBlobStorageAccount, - `${packageName}-${LanguageServerDownloadChannel.stable}`, - azureCDNBlobStorageAccount - ); - } -} - -@injectable() -export abstract class BetaLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { - constructor(serviceContainer: IServiceContainer, packageName: string) { - super( - serviceContainer, - azureBlobStorageAccount, - `${packageName}-${LanguageServerDownloadChannel.beta}`, - azureCDNBlobStorageAccount - ); - } -} - -@injectable() -export abstract class DailyLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { - constructor(serviceContainer: IServiceContainer, packageName: string) { - super( - serviceContainer, - azureBlobStorageAccount, - `${packageName}-${LanguageServerDownloadChannel.daily}`, - azureCDNBlobStorageAccount - ); - } -} diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index 45750a4c9b4d..d32ba7180c0f 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -3,15 +3,16 @@ 'use strict'; -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable } from 'inversify'; import * as querystring from 'querystring'; -import { IApplicationEnvironment, IApplicationShell } from '../common/application/types'; +import { env, UIKind } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../common/application/types'; import { ShowExtensionSurveyPrompt } from '../common/experiments/groups'; import '../common/extensions'; -import { traceDecorators } from '../common/logger'; import { IPlatformService } from '../common/platform/types'; -import { IBrowserService, IExperimentsManager, IPersistentStateFactory, IRandom } from '../common/types'; +import { IBrowserService, IExperimentService, IPersistentStateFactory, IRandom } from '../common/types'; import { Common, ExtensionSurveyBanner } from '../common/utils/localize'; +import { traceDecoratorError } from '../logging'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { IExtensionSingleActivationService } from './types'; @@ -19,7 +20,7 @@ import { IExtensionSingleActivationService } from './types'; // persistent state names, exported to make use of in testing export enum extensionSurveyStateKeys { doNotShowAgain = 'doNotShowExtensionSurveyAgain', - disableSurveyForTime = 'doNotShowExtensionSurveyUntilTime' + disableSurveyForTime = 'doNotShowExtensionSurveyUntilTime', } const timeToDisableSurveyFor = 1000 * 60 * 60 * 24 * 7 * 12; // 12 weeks @@ -27,21 +28,22 @@ const WAIT_TIME_TO_SHOW_SURVEY = 1000 * 60 * 60 * 3; // 3 hours @injectable() export class ExtensionSurveyPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; constructor( @inject(IApplicationShell) private appShell: IApplicationShell, @inject(IBrowserService) private browserService: IBrowserService, @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, @inject(IRandom) private random: IRandom, - @inject(IExperimentsManager) private experiments: IExperimentsManager, + @inject(IExperimentService) private experiments: IExperimentService, @inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment, @inject(IPlatformService) private platformService: IPlatformService, - @optional() private sampleSizePerOneHundredUsers: number = 10, - @optional() private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + private sampleSizePerOneHundredUsers: number = 10, + private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY, ) {} public async activate(): Promise { - if (!this.experiments.inExperiment(ShowExtensionSurveyPrompt.enabled)) { - this.experiments.sendTelemetryIfInExperiment(ShowExtensionSurveyPrompt.control); + if (!(await this.experiments.inExperiment(ShowExtensionSurveyPrompt.experiment))) { return; } const show = this.shouldShowBanner(); @@ -51,11 +53,26 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService setTimeout(() => this.showSurvey().ignoreErrors(), this.waitTimeToShowSurvey); } - @traceDecorators.error('Failed to check whether to display prompt for extension survey') + @traceDecoratorError('Failed to check whether to display prompt for extension survey') public shouldShowBanner(): boolean { + if (env.uiKind === UIKind?.Web) { + return false; + } + + let feedbackEnabled = true; + + const telemetryConfig = this.workspace.getConfiguration('telemetry'); + if (telemetryConfig) { + feedbackEnabled = telemetryConfig.get('feedback.enabled', true); + } + + if (!feedbackEnabled) { + return false; + } + const doNotShowSurveyAgain = this.persistentState.createGlobalPersistentState( extensionSurveyStateKeys.doNotShowAgain, - false + false, ); if (doNotShowSurveyAgain.value) { return false; @@ -63,7 +80,7 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService const isSurveyDisabledForTimeState = this.persistentState.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - timeToDisableSurveyFor + timeToDisableSurveyFor, ); if (isSurveyDisabledForTimeState.value) { return false; @@ -76,36 +93,32 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService return true; } - @traceDecorators.error('Failed to display prompt for extension survey') + @traceDecoratorError('Failed to display prompt for extension survey') public async showSurvey() { - const prompts = [ - ExtensionSurveyBanner.bannerLabelYes(), - ExtensionSurveyBanner.maybeLater(), - Common.doNotShowAgain() - ]; - const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = [ + const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; + const telemetrySelections: ['Yes', 'Maybe later', "Don't show again"] = [ 'Yes', 'Maybe later', - 'Do not show again' + "Don't show again", ]; - const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts); + const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts); sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, { - selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, }); if (!selection) { return; } - if (selection === ExtensionSurveyBanner.bannerLabelYes()) { + if (selection === ExtensionSurveyBanner.bannerLabelYes) { this.launchSurvey(); // Disable survey for a few weeks await this.persistentState .createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - timeToDisableSurveyFor + timeToDisableSurveyFor, ) .updateValue(true); - } else if (selection === Common.doNotShowAgain()) { + } else if (selection === Common.doNotShowAgain) { // Never show the survey again await this.persistentState .createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) @@ -118,7 +131,7 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService o: encodeURIComponent(this.platformService.osType), // platform v: encodeURIComponent(this.appEnvironment.vscodeVersion), e: encodeURIComponent(this.appEnvironment.packageJson.version), // extension version - m: encodeURIComponent(this.appEnvironment.sessionId) + m: encodeURIComponent(this.appEnvironment.sessionId), }); const url = `https://aka.ms/AA5rjx5?${query}`; this.browserService.launch(url); diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts deleted file mode 100644 index 69259b62191a..000000000000 --- a/src/client/activation/jedi.ts +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { inject, injectable } from 'inversify'; -import { - CancellationToken, - CodeLens, - commands, - CompletionContext, - CompletionItem, - CompletionList, - DocumentFilter, - DocumentSymbol, - Event, - Hover, - languages, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - WorkspaceEdit -} from 'vscode'; - -import { PYTHON } from '../common/constants'; -import { traceError } from '../common/logger'; -import { IConfigurationService, IDisposable, IExtensionContext, Resource } from '../common/types'; -import { IShebangCodeLensProvider } from '../interpreter/contracts'; -import { IServiceContainer, IServiceManager } from '../ioc/types'; -import { JediFactory } from '../languageServices/jediProxyFactory'; -import { PythonCompletionItemProvider } from '../providers/completionProvider'; -import { PythonDefinitionProvider } from '../providers/definitionProvider'; -import { PythonHoverProvider } from '../providers/hoverProvider'; -import { PythonObjectDefinitionProvider } from '../providers/objectDefinitionProvider'; -import { PythonReferenceProvider } from '../providers/referenceProvider'; -import { PythonRenameProvider } from '../providers/renameProvider'; -import { PythonSignatureProvider } from '../providers/signatureProvider'; -import { JediSymbolProvider } from '../providers/symbolProvider'; -import { PythonInterpreter } from '../pythonEnvironments/info'; -import { ITestManagementService } from '../testing/types'; -import { BlockFormatProviders } from '../typeFormatters/blockFormatProvider'; -import { OnTypeFormattingDispatcher } from '../typeFormatters/dispatcher'; -import { OnEnterFormatter } from '../typeFormatters/onEnterFormatter'; -import { WorkspaceSymbols } from '../workspaceSymbols/main'; -import { ILanguageServerActivator } from './types'; - -@injectable() -export class JediExtensionActivator implements ILanguageServerActivator { - private static workspaceSymbols: WorkspaceSymbols | undefined; - private readonly context: IExtensionContext; - private jediFactory?: JediFactory; - private readonly documentSelector: DocumentFilter[]; - private renameProvider: PythonRenameProvider | undefined; - private hoverProvider: PythonHoverProvider | undefined; - private definitionProvider: PythonDefinitionProvider | undefined; - private referenceProvider: PythonReferenceProvider | undefined; - private completionProvider: PythonCompletionItemProvider | undefined; - private codeLensProvider: IShebangCodeLensProvider | undefined; - private symbolProvider: JediSymbolProvider | undefined; - private signatureProvider: PythonSignatureProvider | undefined; - private registrations: IDisposable[] = []; - private objectDefinitionProvider: PythonObjectDefinitionProvider | undefined; - - constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { - this.context = this.serviceManager.get(IExtensionContext); - this.documentSelector = PYTHON; - } - - public async start(_resource: Resource, interpreter: PythonInterpreter | undefined): Promise { - if (this.jediFactory) { - throw new Error('Jedi already started'); - } - const context = this.context; - const jediFactory = (this.jediFactory = new JediFactory(interpreter, this.serviceManager)); - context.subscriptions.push(jediFactory); - const serviceContainer = this.serviceManager.get(IServiceContainer); - - this.renameProvider = new PythonRenameProvider(this.serviceManager); - this.definitionProvider = new PythonDefinitionProvider(jediFactory); - this.hoverProvider = new PythonHoverProvider(jediFactory); - this.referenceProvider = new PythonReferenceProvider(jediFactory); - this.completionProvider = new PythonCompletionItemProvider(jediFactory, this.serviceManager); - this.codeLensProvider = this.serviceManager.get(IShebangCodeLensProvider); - this.objectDefinitionProvider = new PythonObjectDefinitionProvider(jediFactory); - this.symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); - this.signatureProvider = new PythonSignatureProvider(jediFactory); - - if (!JediExtensionActivator.workspaceSymbols) { - // Workspace symbols is static because it doesn't rely on the jediFactory. - JediExtensionActivator.workspaceSymbols = new WorkspaceSymbols(serviceContainer); - context.subscriptions.push(JediExtensionActivator.workspaceSymbols); - } - - const testManagementService = this.serviceManager.get(ITestManagementService); - testManagementService - .activate(this.symbolProvider) - .catch((ex) => traceError('Failed to activate Unit Tests', ex)); - } - - public deactivate() { - this.registrations.forEach((r) => r.dispose()); - this.registrations = []; - } - - public activate() { - if ( - this.registrations.length === 0 && - this.renameProvider && - this.definitionProvider && - this.hoverProvider && - this.referenceProvider && - this.completionProvider && - this.codeLensProvider && - this.symbolProvider && - this.signatureProvider - ) { - // Make sure commands are in the registration list that gets disposed when the language server is disconnected from the - // IDE. - this.registrations.push( - commands.registerCommand('python.goToPythonObject', () => - this.objectDefinitionProvider!.goToObjectDefinition() - ) - ); - this.registrations.push(languages.registerRenameProvider(this.documentSelector, this.renameProvider)); - this.registrations.push( - languages.registerDefinitionProvider(this.documentSelector, this.definitionProvider) - ); - this.registrations.push(languages.registerHoverProvider(this.documentSelector, this.hoverProvider)); - this.registrations.push(languages.registerReferenceProvider(this.documentSelector, this.referenceProvider)); - this.registrations.push( - languages.registerCompletionItemProvider(this.documentSelector, this.completionProvider, '.') - ); - this.registrations.push(languages.registerCodeLensProvider(this.documentSelector, this.codeLensProvider)); - const onTypeDispatcher = new OnTypeFormattingDispatcher({ - '\n': new OnEnterFormatter(), - ':': new BlockFormatProviders() - }); - const onTypeTriggers = onTypeDispatcher.getTriggerCharacters(); - if (onTypeTriggers) { - this.registrations.push( - languages.registerOnTypeFormattingEditProvider( - PYTHON, - onTypeDispatcher, - onTypeTriggers.first, - ...onTypeTriggers.more - ) - ); - } - this.registrations.push( - languages.registerDocumentSymbolProvider(this.documentSelector, this.symbolProvider) - ); - const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); - if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - this.registrations.push( - languages.registerSignatureHelpProvider(this.documentSelector, this.signatureProvider, '(', ',') - ); - } - } - } - - public provideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken - ): ProviderResult { - if (this.renameProvider) { - return this.renameProvider.provideRenameEdits(document, position, newName, token); - } - } - public provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken - ): ProviderResult { - if (this.definitionProvider) { - return this.definitionProvider.provideDefinition(document, position, token); - } - } - public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - if (this.hoverProvider) { - return this.hoverProvider.provideHover(document, position, token); - } - } - public provideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken - ): ProviderResult { - if (this.referenceProvider) { - return this.referenceProvider.provideReferences(document, position, context, token); - } - } - public provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - _context: CompletionContext - ): ProviderResult { - if (this.completionProvider) { - return this.completionProvider.provideCompletionItems(document, position, token); - } - } - - public resolveCompletionItem(item: CompletionItem, token: CancellationToken): ProviderResult { - if (this.completionProvider) { - return this.completionProvider.resolveCompletionItem(item, token); - } - } - - public get onDidChangeCodeLenses(): Event | undefined { - return this.codeLensProvider ? this.codeLensProvider.onDidChangeCodeLenses : undefined; - } - public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { - if (this.codeLensProvider) { - return this.codeLensProvider.provideCodeLenses(document, token); - } - } - public provideDocumentSymbols( - document: TextDocument, - token: CancellationToken - ): ProviderResult { - if (this.symbolProvider) { - return this.symbolProvider.provideDocumentSymbols(document, token); - } - } - public provideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - _context: SignatureHelpContext - ): ProviderResult { - if (this.signatureProvider) { - return this.signatureProvider.provideSignatureHelp(document, position, token); - } - } - - public dispose(): void { - this.registrations.forEach((r) => r.dispose()); - if (this.jediFactory) { - this.jediFactory.dispose(); - } - } -} diff --git a/src/client/activation/jedi/analysisOptions.ts b/src/client/activation/jedi/analysisOptions.ts new file mode 100644 index 000000000000..007008dc9b13 --- /dev/null +++ b/src/client/activation/jedi/analysisOptions.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IConfigurationService, Resource } from '../../common/types'; + +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { LanguageServerAnalysisOptionsWithEnv } from '../common/analysisOptions'; +import { ILanguageServerOutputChannel } from '../types'; + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types, class-methods-use-this */ + +export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsWithEnv { + private resource: Resource | undefined; + + private interpreter: PythonEnvironment | undefined; + + constructor( + envVarsProvider: IEnvironmentVariablesProvider, + lsOutputChannel: ILanguageServerOutputChannel, + private readonly configurationService: IConfigurationService, + workspace: IWorkspaceService, + ) { + super(envVarsProvider, lsOutputChannel, workspace); + this.resource = undefined; + } + + public async initialize(resource: Resource, interpreter: PythonEnvironment | undefined) { + this.resource = resource; + this.interpreter = interpreter; + return super.initialize(resource, interpreter); + } + + protected getWorkspaceFolder(): WorkspaceFolder | undefined { + return this.workspace.getWorkspaceFolder(this.resource); + } + + protected async getInitializationOptions() { + const pythonSettings = this.configurationService.getSettings(this.resource); + const workspacePath = this.getWorkspaceFolder()?.uri.fsPath; + const extraPaths = pythonSettings.autoComplete + ? pythonSettings.autoComplete.extraPaths.map((extraPath) => { + if (path.isAbsolute(extraPath)) { + return extraPath; + } + return workspacePath ? path.join(workspacePath, extraPath) : ''; + }) + : []; + + if (workspacePath) { + extraPaths.unshift(workspacePath); + } + + const distinctExtraPaths = extraPaths + .filter((value) => value.length > 0) + .filter((value, index, self) => self.indexOf(value) === index); + + return { + markupKindPreferred: 'markdown', + completion: { + resolveEagerly: false, + disableSnippets: true, + }, + diagnostics: { + enable: true, + didOpen: true, + didSave: true, + didChange: true, + }, + hover: { + disable: { + keyword: { + all: true, + }, + }, + }, + workspace: { + extraPaths: distinctExtraPaths, + environmentPath: this.interpreter?.path, + symbols: { + // 0 means remove limit on number of workspace symbols returned + maxSymbols: 0, + }, + }, + semantic_tokens: { + enable: true, + }, + }; + } +} diff --git a/src/client/activation/jedi/languageClientFactory.ts b/src/client/activation/jedi/languageClientFactory.ts new file mode 100644 index 000000000000..70bd65da8d0d --- /dev/null +++ b/src/client/activation/jedi/languageClientFactory.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; + +import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; +import { Resource } from '../../common/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ILanguageClientFactory } from '../types'; + +const languageClientName = 'Python Jedi'; + +export class JediLanguageClientFactory implements ILanguageClientFactory { + constructor(private interpreterService: IInterpreterService) {} + + public async createLanguageClient( + resource: Resource, + _interpreter: PythonEnvironment | undefined, + clientOptions: LanguageClientOptions, + ): Promise { + // Just run the language server using a module + const lsScriptPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'run-jedi-language-server.py'); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const serverOptions: ServerOptions = { + command: interpreter ? interpreter.path : 'python', + args: [lsScriptPath], + }; + + return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); + } +} diff --git a/src/client/activation/jedi/languageClientMiddleware.ts b/src/client/activation/jedi/languageClientMiddleware.ts new file mode 100644 index 000000000000..c8bb99629946 --- /dev/null +++ b/src/client/activation/jedi/languageClientMiddleware.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceContainer } from '../../ioc/types'; +import { LanguageClientMiddleware } from '../languageClientMiddleware'; +import { LanguageServerType } from '../types'; + +export class JediLanguageClientMiddleware extends LanguageClientMiddleware { + public constructor(serviceContainer: IServiceContainer, serverVersion?: string) { + super(serviceContainer, LanguageServerType.Jedi, serverVersion); + } +} diff --git a/src/client/activation/jedi/languageServerProxy.ts b/src/client/activation/jedi/languageServerProxy.ts new file mode 100644 index 000000000000..d7ffe8328b9e --- /dev/null +++ b/src/client/activation/jedi/languageServerProxy.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import '../../common/extensions'; +import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; + +import { ChildProcess } from 'child_process'; +import { Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { JediLanguageClientMiddleware } from './languageClientMiddleware'; +import { ProgressReporting } from '../progress'; +import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; +import { killPid } from '../../common/process/rawProcessApis'; +import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; + +export class JediLanguageServerProxy implements ILanguageServerProxy { + private languageClient: LanguageClient | undefined; + + private readonly disposables: Disposable[] = []; + + private lsVersion: string | undefined; + + constructor(private readonly factory: ILanguageClientFactory) {} + + private static versionTelemetryProps(instance: JediLanguageServerProxy) { + return { + lsVersion: instance.lsVersion, + }; + } + + @traceDecoratorVerbose('Disposing language server') + public dispose(): void { + this.stop().ignoreErrors(); + } + + @traceDecoratorError('Failed to start language server') + @captureTelemetry( + EventName.JEDI_LANGUAGE_SERVER_ENABLED, + undefined, + true, + undefined, + JediLanguageServerProxy.versionTelemetryProps, + ) + public async start( + resource: Resource, + interpreter: PythonEnvironment | undefined, + options: LanguageClientOptions, + ): Promise { + this.lsVersion = + (options.middleware ? (options.middleware).serverVersion : undefined) ?? + '0.19.3'; + + try { + const client = await this.factory.createLanguageClient(resource, interpreter, options); + this.registerHandlers(client); + await client.start(); + this.languageClient = client; + } catch (ex) { + traceError('Failed to start language server:', ex); + throw new Error('Launching Jedi language server using python failed, see output.'); + } + } + + @traceDecoratorVerbose('Stopping language server') + public async stop(): Promise { + while (this.disposables.length > 0) { + const d = this.disposables.shift()!; + d.dispose(); + } + + if (this.languageClient) { + const client = this.languageClient; + this.languageClient = undefined; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pid: number | undefined = ((client as any)._serverProcess as ChildProcess)?.pid; + const killServer = () => { + if (pid) { + killPid(pid); + } + }; + + try { + await client.stop(); + await client.dispose(); + killServer(); + } catch (ex) { + traceError('Stopping language client failed', ex); + killServer(); + } + } + } + + // eslint-disable-next-line class-methods-use-this + public loadExtension(): void { + // No body. + } + + @captureTelemetry( + EventName.JEDI_LANGUAGE_SERVER_READY, + undefined, + true, + undefined, + JediLanguageServerProxy.versionTelemetryProps, + ) + private registerHandlers(client: LanguageClient) { + const progressReporting = new ProgressReporting(client); + this.disposables.push(progressReporting); + } +} diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts new file mode 100644 index 000000000000..bafdcc735a12 --- /dev/null +++ b/src/client/activation/jedi/manager.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import '../../common/extensions'; + +import { ICommandManager } from '../../common/application/types'; +import { IDisposable, Resource } from '../../common/types'; +import { debounceSync } from '../../common/utils/decorators'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { Commands } from '../commands'; +import { JediLanguageClientMiddleware } from './languageClientMiddleware'; +import { ILanguageServerAnalysisOptions, ILanguageServerManager, ILanguageServerProxy } from '../types'; +import { traceDecoratorError, traceDecoratorVerbose, traceVerbose } from '../../logging'; + +export class JediLanguageServerManager implements ILanguageServerManager { + private resource!: Resource; + + private interpreter: PythonEnvironment | undefined; + + private middleware: JediLanguageClientMiddleware | undefined; + + private disposables: IDisposable[] = []; + + private static commandDispose: IDisposable; + + private connected = false; + + private lsVersion: string | undefined; + + constructor( + private readonly serviceContainer: IServiceContainer, + private readonly analysisOptions: ILanguageServerAnalysisOptions, + private readonly languageServerProxy: ILanguageServerProxy, + commandManager: ICommandManager, + ) { + if (JediLanguageServerManager.commandDispose) { + JediLanguageServerManager.commandDispose.dispose(); + } + JediLanguageServerManager.commandDispose = commandManager.registerCommand(Commands.RestartLS, () => { + this.restartLanguageServer().ignoreErrors(); + }); + } + + private static versionTelemetryProps(instance: JediLanguageServerManager) { + return { + lsVersion: instance.lsVersion, + }; + } + + public dispose(): void { + this.stopLanguageServer().ignoreErrors(); + JediLanguageServerManager.commandDispose.dispose(); + this.disposables.forEach((d) => d.dispose()); + } + + @traceDecoratorError('Failed to start language server') + public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { + this.resource = resource; + this.interpreter = interpreter; + this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); + + try { + // Version is actually hardcoded in our requirements.txt. + const requirementsTxt = await fs.readFile( + path.join(EXTENSION_ROOT_DIR, 'python_files', 'jedilsp_requirements', 'requirements.txt'), + 'utf-8', + ); + + // Search using a regex in the text + const match = /jedi-language-server==([0-9\.]*)/.exec(requirementsTxt); + if (match && match.length === 2) { + [, this.lsVersion] = match; + } + } catch (ex) { + // Getting version here is best effort and does not affect how LS works and + // failing to get version should not stop LS from working. + traceVerbose('Failed to get jedi-language-server version: ', ex); + } + + await this.analysisOptions.initialize(resource, interpreter); + await this.startLanguageServer(); + } + + public connect(): void { + if (!this.connected) { + this.connected = true; + this.middleware?.connect(); + } + } + + public disconnect(): void { + if (this.connected) { + this.connected = false; + this.middleware?.disconnect(); + } + } + + @debounceSync(1000) + protected restartLanguageServerDebounced(): void { + this.restartLanguageServer().ignoreErrors(); + } + + @traceDecoratorError('Failed to restart language server') + @traceDecoratorVerbose('Restarting language server') + protected async restartLanguageServer(): Promise { + await this.stopLanguageServer(); + await this.startLanguageServer(); + } + + @captureTelemetry( + EventName.JEDI_LANGUAGE_SERVER_STARTUP, + undefined, + true, + undefined, + JediLanguageServerManager.versionTelemetryProps, + ) + @traceDecoratorVerbose('Starting language server') + protected async startLanguageServer(): Promise { + const options = await this.analysisOptions.getAnalysisOptions(); + this.middleware = new JediLanguageClientMiddleware(this.serviceContainer, this.lsVersion); + options.middleware = this.middleware; + + // Make sure the middleware is connected if we restart and we we're already connected. + if (this.connected) { + this.middleware.connect(); + } + + // Then use this middleware to start a new language client. + await this.languageServerProxy.start(this.resource, this.interpreter, options); + } + + @traceDecoratorVerbose('Stopping language server') + protected async stopLanguageServer(): Promise { + if (this.languageServerProxy) { + await this.languageServerProxy.stop(); + } + } +} diff --git a/src/client/activation/languageClientMiddleware.ts b/src/client/activation/languageClientMiddleware.ts index 2232af85e039..d3d1e0c3c171 100644 --- a/src/client/activation/languageClientMiddleware.ts +++ b/src/client/activation/languageClientMiddleware.ts @@ -1,476 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; -import { - CancellationToken, - CodeAction, - CodeActionContext, - CodeLens, - Command, - CompletionContext, - CompletionItem, - Declaration as VDeclaration, - Definition, - DefinitionLink, - Diagnostic, - DocumentHighlight, - DocumentLink, - DocumentSymbol, - FormattingOptions, - Location, - Position, - Position as VPosition, - ProviderResult, - Range, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - TextEdit, - Uri, - WorkspaceEdit -} from 'vscode'; -import { - ConfigurationParams, - ConfigurationRequest, - HandleDiagnosticsSignature, - HandlerResult, - Middleware, - PrepareRenameSignature, - ProvideCodeActionsSignature, - ProvideCodeLensesSignature, - ProvideCompletionItemsSignature, - ProvideDefinitionSignature, - ProvideDocumentFormattingEditsSignature, - ProvideDocumentHighlightsSignature, - ProvideDocumentLinksSignature, - ProvideDocumentRangeFormattingEditsSignature, - ProvideDocumentSymbolsSignature, - ProvideHoverSignature, - ProvideOnTypeFormattingEditsSignature, - ProvideReferencesSignature, - ProvideRenameEditsSignature, - ProvideSignatureHelpSignature, - ProvideWorkspaceSymbolsSignature, - ResolveCodeLensSignature, - ResolveCompletionItemSignature, - ResolveDocumentLinkSignature, - ResponseError -} from 'vscode-languageclient/node'; -import { ProvideDeclarationSignature } from 'vscode-languageclient/lib/common/declaration'; -import { HiddenFilePrefix } from '../common/constants'; -import { CollectLSRequestTiming, CollectNodeLSRequestTiming } from '../common/experiments/groups'; -import { IConfigurationService, IExperimentsManager } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; +import { IServiceContainer } from '../ioc/types'; import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { LanguageServerType } from './types'; - -// Only send 100 events per hour. -const globalDebounce = 1000 * 60 * 60; -const globalLimit = 100; - -// For calls that are more likely to happen during a session (hover, completion, document symbols). -const debounceFrequentCall = 1000 * 60 * 5; - -// For calls that are less likely to happen during a session (go-to-def, workspace symbols). -const debounceRareCall = 1000 * 60; - -export class LanguageClientMiddleware implements Middleware { - // These are public so that the captureTelemetryForLSPMethod decorator can access them. - public readonly eventName: EventName | undefined; - public readonly lastCaptured = new Map(); - public nextWindow: number = 0; - public eventCount: number = 0; - - public workspace = { - // tslint:disable:no-any - configuration: ( - params: ConfigurationParams, - token: CancellationToken, - next: ConfigurationRequest.HandlerSignature - ): HandlerResult => { - // Hand-collapse "Thenable | Thenable | Thenable" into just "Thenable" to make TS happy. - const result: any[] | ResponseError | Thenable> = next(params, token); - - // For backwards compatibility, set python.pythonPath to the configured - // value as though it were in the user's settings.json file. - const addPythonPath = (settings: any[] | ResponseError) => { - if (settings instanceof ResponseError) { - return settings; - } - - params.items.forEach((item, i) => { - if (item.section === 'python') { - const uri = item.scopeUri ? Uri.parse(item.scopeUri) : undefined; - settings[i].pythonPath = this.configService.getSettings(uri).pythonPath; - } - }); - - return settings; - }; - - if (isThenable(result)) { - return result.then(addPythonPath); - } - - return addPythonPath(result); - } - // tslint:enable:no-any - }; - - private connected = false; // Default to not forwarding to VS code. - - public constructor( - experimentsManager: IExperimentsManager, - private readonly configService: IConfigurationService, - serverType: LanguageServerType, - public readonly serverVersion?: string - ) { - this.handleDiagnostics = this.handleDiagnostics.bind(this); // VS Code calls function without context. - - let group: { experiment: string; control: string } | undefined; - - if (serverType === LanguageServerType.Microsoft) { - this.eventName = EventName.PYTHON_LANGUAGE_SERVER_REQUEST; - group = CollectLSRequestTiming; - } else if (serverType === LanguageServerType.Node) { - this.eventName = EventName.LANGUAGE_SERVER_REQUEST; - group = CollectNodeLSRequestTiming; - } else { - return; - } - - if (!experimentsManager.inExperiment(group.experiment)) { - this.eventName = undefined; - experimentsManager.sendTelemetryIfInExperiment(group.control); - } - } - - public connect() { - this.connected = true; - } - - public disconnect() { - this.connected = false; - } - - @captureTelemetryForLSPMethod('textDocument/completion', debounceFrequentCall) - public provideCompletionItem( - document: TextDocument, - position: Position, - context: CompletionContext, - token: CancellationToken, - next: ProvideCompletionItemsSignature - ) { - if (this.connected) { - return next(document, position, context, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/hover', debounceFrequentCall) - public provideHover( - document: TextDocument, - position: Position, - token: CancellationToken, - next: ProvideHoverSignature - ) { - if (this.connected) { - return next(document, position, token); - } - } - - public handleDiagnostics(uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) { - if (this.connected) { - // Skip sending if this is a special file. - const filePath = uri.fsPath; - const baseName = filePath ? path.basename(filePath) : undefined; - if (!baseName || !baseName.startsWith(HiddenFilePrefix)) { - next(uri, diagnostics); - } - } - } - - @captureTelemetryForLSPMethod('completionItem/resolve', debounceFrequentCall) - public resolveCompletionItem( - item: CompletionItem, - token: CancellationToken, - next: ResolveCompletionItemSignature - ): ProviderResult { - if (this.connected) { - return next(item, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/signatureHelp', debounceFrequentCall) - public provideSignatureHelp( - document: TextDocument, - position: Position, - context: SignatureHelpContext, - token: CancellationToken, - next: ProvideSignatureHelpSignature - ): ProviderResult { - if (this.connected) { - return next(document, position, context, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/definition', debounceRareCall) - public provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken, - next: ProvideDefinitionSignature - ): ProviderResult { - if (this.connected) { - return next(document, position, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/references', debounceRareCall) - public provideReferences( - document: TextDocument, - position: Position, - options: { - includeDeclaration: boolean; - }, - token: CancellationToken, - next: ProvideReferencesSignature - ): ProviderResult { - if (this.connected) { - return next(document, position, options, token); - } - } - - public provideDocumentHighlights( - document: TextDocument, - position: Position, - token: CancellationToken, - next: ProvideDocumentHighlightsSignature - ): ProviderResult { - if (this.connected) { - return next(document, position, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/documentSymbol', debounceFrequentCall) - public provideDocumentSymbols( - document: TextDocument, - token: CancellationToken, - next: ProvideDocumentSymbolsSignature - ): ProviderResult { - if (this.connected) { - return next(document, token); - } - } - - @captureTelemetryForLSPMethod('workspace/symbol', debounceRareCall) - public provideWorkspaceSymbols( - query: string, - token: CancellationToken, - next: ProvideWorkspaceSymbolsSignature - ): ProviderResult { - if (this.connected) { - return next(query, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/codeAction', debounceFrequentCall) - public provideCodeActions( - document: TextDocument, - range: Range, - context: CodeActionContext, - token: CancellationToken, - next: ProvideCodeActionsSignature - ): ProviderResult<(Command | CodeAction)[]> { - if (this.connected) { - return next(document, range, context, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/codeLens', debounceFrequentCall) - public provideCodeLenses( - document: TextDocument, - token: CancellationToken, - next: ProvideCodeLensesSignature - ): ProviderResult { - if (this.connected) { - return next(document, token); - } - } - - @captureTelemetryForLSPMethod('codeLens/resolve', debounceFrequentCall) - public resolveCodeLens( - codeLens: CodeLens, - token: CancellationToken, - next: ResolveCodeLensSignature - ): ProviderResult { - if (this.connected) { - return next(codeLens, token); - } - } - - public provideDocumentFormattingEdits( - document: TextDocument, - options: FormattingOptions, - token: CancellationToken, - next: ProvideDocumentFormattingEditsSignature - ): ProviderResult { - if (this.connected) { - return next(document, options, token); - } - } - - public provideDocumentRangeFormattingEdits( - document: TextDocument, - range: Range, - options: FormattingOptions, - token: CancellationToken, - next: ProvideDocumentRangeFormattingEditsSignature - ): ProviderResult { - if (this.connected) { - return next(document, range, options, token); - } - } - - public provideOnTypeFormattingEdits( - document: TextDocument, - position: Position, - ch: string, - options: FormattingOptions, - token: CancellationToken, - next: ProvideOnTypeFormattingEditsSignature - ): ProviderResult { - if (this.connected) { - return next(document, position, ch, options, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/rename', debounceRareCall) - public provideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken, - next: ProvideRenameEditsSignature - ): ProviderResult { - if (this.connected) { - return next(document, position, newName, token); - } - } - - @captureTelemetryForLSPMethod('textDocument/prepareRename', debounceRareCall) - public prepareRename( - document: TextDocument, - position: Position, - token: CancellationToken, - next: PrepareRenameSignature - ): ProviderResult< - | Range - | { - range: Range; - placeholder: string; - } - > { - if (this.connected) { - return next(document, position, token); - } - } - public provideDocumentLinks( - document: TextDocument, - token: CancellationToken, - next: ProvideDocumentLinksSignature - ): ProviderResult { - if (this.connected) { - return next(document, token); - } - } - - public resolveDocumentLink( - link: DocumentLink, - token: CancellationToken, - next: ResolveDocumentLinkSignature - ): ProviderResult { - if (this.connected) { - return next(link, token); - } - } +import { LanguageClientMiddlewareBase } from './languageClientMiddlewareBase'; +import { LanguageServerType } from './types'; - @captureTelemetryForLSPMethod('textDocument/declaration', debounceRareCall) - public provideDeclaration( - document: TextDocument, - position: VPosition, - token: CancellationToken, - next: ProvideDeclarationSignature - ): ProviderResult { - if (this.connected) { - return next(document, position, token); - } +export class LanguageClientMiddleware extends LanguageClientMiddlewareBase { + public constructor(serviceContainer: IServiceContainer, serverType: LanguageServerType, serverVersion?: string) { + super(serviceContainer, serverType, sendTelemetryEvent, serverVersion); } } - -function captureTelemetryForLSPMethod(method: string, debounceMilliseconds: number) { - // tslint:disable-next-line:no-function-expression no-any - return function (_target: Object, _propertyKey: string, descriptor: TypedPropertyDescriptor) { - const originalMethod = descriptor.value; - - // tslint:disable-next-line:no-any - descriptor.value = function (this: LanguageClientMiddleware, ...args: any[]) { - const eventName = this.eventName; - if (!eventName) { - return originalMethod.apply(this, args); - } - - const now = Date.now(); - - if (now > this.nextWindow) { - // Past the end of the last window, reset. - this.nextWindow = now + globalDebounce; - this.eventCount = 0; - } else if (this.eventCount >= globalLimit) { - // Sent too many events in this window, don't send. - return originalMethod.apply(this, args); - } - - const lastCapture = this.lastCaptured.get(method); - if (lastCapture && now - lastCapture < debounceMilliseconds) { - return originalMethod.apply(this, args); - } - - this.lastCaptured.set(method, now); - this.eventCount += 1; - - // Replace all slashes in the method name so it doesn't get scrubbed by vscode-extension-telemetry. - const formattedMethod = method.replace(/\//g, '.'); - - const properties = { - lsVersion: this.serverVersion || 'unknown', - method: formattedMethod - }; - - const stopWatch = new StopWatch(); - // tslint:disable-next-line:no-unsafe-any - const result = originalMethod.apply(this, args); - - // tslint:disable-next-line:no-unsafe-any - if (result && isThenable(result)) { - result.then(() => { - sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); - }); - } else { - sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); - } - - return result; - }; - - return descriptor; - }; -} - -// tslint:disable-next-line: no-any -function isThenable(v: any): v is Thenable { - return typeof v?.then === 'function'; -} diff --git a/src/client/activation/languageClientMiddlewareBase.ts b/src/client/activation/languageClientMiddlewareBase.ts new file mode 100644 index 000000000000..f1e102a4081d --- /dev/null +++ b/src/client/activation/languageClientMiddlewareBase.ts @@ -0,0 +1,596 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { CancellationToken, Diagnostic, Disposable, Uri } from 'vscode'; +import { + ConfigurationParams, + ConfigurationRequest, + HandleDiagnosticsSignature, + LSPObject, + Middleware, + ResponseError, +} from 'vscode-languageclient'; +import { ConfigurationItem } from 'vscode-languageserver-protocol'; + +import { HiddenFilePrefix } from '../common/constants'; +import { createDeferred, isThenable } from '../common/utils/async'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { EventName } from '../telemetry/constants'; +import { LanguageServerType } from './types'; + +// Only send 100 events per hour. +const globalDebounce = 1000 * 60 * 60; +const globalLimit = 100; + +// For calls that are more likely to happen during a session (hover, completion, document symbols). +const debounceFrequentCall = 1000 * 60 * 5; + +// For calls that are less likely to happen during a session (go-to-def, workspace symbols). +const debounceRareCall = 1000 * 60; + +type Awaited = T extends PromiseLike ? U : T; +type MiddleWareMethods = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [P in keyof Middleware]-?: NonNullable extends (...args: any) => any ? Middleware[P] : never; +}; + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable prefer-rest-params */ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface SendTelemetryEventFunc { + (eventName: EventName, measuresOrDurationMs?: Record | number, properties?: any, ex?: Error): void; +} + +export class LanguageClientMiddlewareBase implements Middleware { + private readonly eventName: EventName | undefined; + + private readonly lastCaptured = new Map(); + + private nextWindow = 0; + + private eventCount = 0; + + public workspace = { + configuration: async ( + params: ConfigurationParams, + token: CancellationToken, + next: ConfigurationRequest.HandlerSignature, + ) => { + if (!this.serviceContainer) { + return next(params, token); + } + + const interpreterService = this.serviceContainer.get(IInterpreterService); + const envService = this.serviceContainer.get(IEnvironmentVariablesProvider); + + let settings = next(params, token); + if (isThenable(settings)) { + settings = await settings; + } + if (settings instanceof ResponseError) { + return settings; + } + + for (const [i, item] of params.items.entries()) { + if (item.section === 'python') { + const uri = item.scopeUri ? Uri.parse(item.scopeUri) : undefined; + // For backwards compatibility, set python.pythonPath to the configured + // value as though it were in the user's settings.json file. + // As this is for backwards compatibility, `ConfigService.pythonPath` + // can be considered as active interpreter path. + const settingDict: LSPObject & { pythonPath: string; _envPYTHONPATH: string } = settings[ + i + ] as LSPObject & { pythonPath: string; _envPYTHONPATH: string }; + settingDict.pythonPath = (await interpreterService.getActiveInterpreter(uri))?.path ?? 'python'; + + const env = await envService.getEnvironmentVariables(uri); + const envPYTHONPATH = env.PYTHONPATH; + if (envPYTHONPATH) { + settingDict._envPYTHONPATH = envPYTHONPATH; + } + } + + this.configurationHook(item, settings[i] as LSPObject); + } + + return settings; + }, + }; + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + protected configurationHook(_item: ConfigurationItem, _settings: LSPObject): void {} + + private get connected(): Promise { + return this.connectedPromise.promise; + } + + protected notebookAddon: (Middleware & Disposable) | undefined; + + private connectedPromise = createDeferred(); + + public constructor( + readonly serviceContainer: IServiceContainer | undefined, + serverType: LanguageServerType, + public readonly sendTelemetryEventFunc: SendTelemetryEventFunc, + public readonly serverVersion?: string, + ) { + this.handleDiagnostics = this.handleDiagnostics.bind(this); // VS Code calls function without context. + this.didOpen = this.didOpen.bind(this); + this.didSave = this.didSave.bind(this); + this.didChange = this.didChange.bind(this); + this.didClose = this.didClose.bind(this); + this.willSave = this.willSave.bind(this); + this.willSaveWaitUntil = this.willSaveWaitUntil.bind(this); + + if (serverType === LanguageServerType.Node) { + this.eventName = EventName.LANGUAGE_SERVER_REQUEST; + } else if (serverType === LanguageServerType.Jedi) { + this.eventName = EventName.JEDI_LANGUAGE_SERVER_REQUEST; + } + } + + public connect() { + this.connectedPromise.resolve(true); + } + + public disconnect() { + this.connectedPromise = createDeferred(); + this.connectedPromise.resolve(false); + } + + public didChange() { + return this.callNext('didChange', arguments); + } + + public didOpen() { + // Special case, open and close happen before we connect. + return this.callNext('didOpen', arguments); + } + + public didClose() { + // Special case, open and close happen before we connect. + return this.callNext('didClose', arguments); + } + + public didSave() { + return this.callNext('didSave', arguments); + } + + public willSave() { + return this.callNext('willSave', arguments); + } + + public willSaveWaitUntil() { + return this.callNext('willSaveWaitUntil', arguments); + } + + public async didOpenNotebook() { + return this.callNotebooksNext('didOpen', arguments); + } + + public async didSaveNotebook() { + return this.callNotebooksNext('didSave', arguments); + } + + public async didChangeNotebook() { + return this.callNotebooksNext('didChange', arguments); + } + + public async didCloseNotebook() { + return this.callNotebooksNext('didClose', arguments); + } + + notebooks = { + didOpen: this.didOpenNotebook.bind(this), + didSave: this.didSaveNotebook.bind(this), + didChange: this.didChangeNotebook.bind(this), + didClose: this.didCloseNotebook.bind(this), + }; + + public async provideCompletionItem() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/completion', + debounceFrequentCall, + 'provideCompletionItem', + arguments, + (_, result) => { + if (!result) { + return { resultLength: 0 }; + } + const resultLength = Array.isArray(result) ? result.length : result.items.length; + return { resultLength }; + }, + ); + } + } + + public async provideHover() { + if (await this.connected) { + return this.callNextAndSendTelemetry('textDocument/hover', debounceFrequentCall, 'provideHover', arguments); + } + } + + public async handleDiagnostics(uri: Uri, _diagnostics: Diagnostic[], _next: HandleDiagnosticsSignature) { + if (await this.connected) { + // Skip sending if this is a special file. + const filePath = uri.fsPath; + const baseName = filePath ? path.basename(filePath) : undefined; + if (!baseName || !baseName.startsWith(HiddenFilePrefix)) { + return this.callNext('handleDiagnostics', arguments); + } + } + } + + public async resolveCompletionItem() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'completionItem/resolve', + debounceFrequentCall, + 'resolveCompletionItem', + arguments, + ); + } + } + + public async provideSignatureHelp() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/signatureHelp', + debounceFrequentCall, + 'provideSignatureHelp', + arguments, + ); + } + } + + public async provideDefinition() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/definition', + debounceRareCall, + 'provideDefinition', + arguments, + ); + } + } + + public async provideReferences() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/references', + debounceRareCall, + 'provideReferences', + arguments, + ); + } + } + + public async provideDocumentHighlights() { + if (await this.connected) { + return this.callNext('provideDocumentHighlights', arguments); + } + } + + public async provideDocumentSymbols() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/documentSymbol', + debounceFrequentCall, + 'provideDocumentSymbols', + arguments, + ); + } + } + + public async provideWorkspaceSymbols() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'workspace/symbol', + debounceRareCall, + 'provideWorkspaceSymbols', + arguments, + ); + } + } + + public async provideCodeActions() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/codeAction', + debounceFrequentCall, + 'provideCodeActions', + arguments, + ); + } + } + + public async provideCodeLenses() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/codeLens', + debounceFrequentCall, + 'provideCodeLenses', + arguments, + ); + } + } + + public async resolveCodeLens() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'codeLens/resolve', + debounceFrequentCall, + 'resolveCodeLens', + arguments, + ); + } + } + + public async provideDocumentFormattingEdits() { + if (await this.connected) { + return this.callNext('provideDocumentFormattingEdits', arguments); + } + } + + public async provideDocumentRangeFormattingEdits() { + if (await this.connected) { + return this.callNext('provideDocumentRangeFormattingEdits', arguments); + } + } + + public async provideOnTypeFormattingEdits() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/onTypeFormatting', + debounceFrequentCall, + 'provideOnTypeFormattingEdits', + arguments, + ); + } + } + + public async provideRenameEdits() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/rename', + debounceRareCall, + 'provideRenameEdits', + arguments, + ); + } + } + + public async prepareRename() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/prepareRename', + debounceRareCall, + 'prepareRename', + arguments, + ); + } + } + + public async provideDocumentLinks() { + if (await this.connected) { + return this.callNext('provideDocumentLinks', arguments); + } + } + + public async resolveDocumentLink() { + if (await this.connected) { + return this.callNext('resolveDocumentLink', arguments); + } + } + + public async provideDeclaration() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/declaration', + debounceRareCall, + 'provideDeclaration', + arguments, + ); + } + } + + public async provideTypeDefinition() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/typeDefinition', + debounceRareCall, + 'provideTypeDefinition', + arguments, + ); + } + } + + public async provideImplementation() { + if (await this.connected) { + return this.callNext('provideImplementation', arguments); + } + } + + public async provideDocumentColors() { + if (await this.connected) { + return this.callNext('provideDocumentColors', arguments); + } + } + + public async provideColorPresentations() { + if (await this.connected) { + return this.callNext('provideColorPresentations', arguments); + } + } + + public async provideFoldingRanges() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/foldingRange', + debounceFrequentCall, + 'provideFoldingRanges', + arguments, + ); + } + } + + public async provideSelectionRanges() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/selectionRange', + debounceRareCall, + 'provideSelectionRanges', + arguments, + ); + } + } + + public async prepareCallHierarchy() { + if (await this.connected) { + return this.callNext('prepareCallHierarchy', arguments); + } + } + + public async provideCallHierarchyIncomingCalls() { + if (await this.connected) { + return this.callNext('provideCallHierarchyIncomingCalls', arguments); + } + } + + public async provideCallHierarchyOutgoingCalls() { + if (await this.connected) { + return this.callNext('provideCallHierarchyOutgoingCalls', arguments); + } + } + + public async provideDocumentSemanticTokens() { + if (await this.connected) { + return this.callNext('provideDocumentSemanticTokens', arguments); + } + } + + public async provideDocumentSemanticTokensEdits() { + if (await this.connected) { + return this.callNext('provideDocumentSemanticTokensEdits', arguments); + } + } + + public async provideDocumentRangeSemanticTokens() { + if (await this.connected) { + return this.callNext('provideDocumentRangeSemanticTokens', arguments); + } + } + + public async provideLinkedEditingRange() { + if (await this.connected) { + return this.callNext('provideLinkedEditingRange', arguments); + } + } + + private callNext(funcName: keyof Middleware, args: IArguments) { + // This function uses the last argument to call the 'next' item. If we're allowing notebook + // middleware, it calls into the notebook middleware first. + if (this.notebookAddon && (this.notebookAddon as any)[funcName]) { + // It would be nice to use args.callee, but not supported in strict mode + return (this.notebookAddon as any)[funcName](...args); + } + + return args[args.length - 1](...args); + } + + private callNotebooksNext(funcName: 'didOpen' | 'didSave' | 'didChange' | 'didClose', args: IArguments) { + // This function uses the last argument to call the 'next' item. If we're allowing notebook + // middleware, it calls into the notebook middleware first. + if (this.notebookAddon?.notebooks && (this.notebookAddon.notebooks as any)[funcName]) { + // It would be nice to use args.callee, but not supported in strict mode + return (this.notebookAddon.notebooks as any)[funcName](...args); + } + + return args[args.length - 1](...args); + } + + private callNextAndSendTelemetry( + lspMethod: string, + debounceMilliseconds: number, + funcName: T, + args: IArguments, + lazyMeasures?: (this_: any, result: Awaited>) => Record, + ): ReturnType { + const now = Date.now(); + const stopWatch = new StopWatch(); + let calledNext = false; + // Change the 'last' argument (which is our next) in order to track if + // telemetry should be sent or not. + const changedArgs = [...args]; + + // Track whether or not the middleware called the 'next' function (which means it actually sent a request) + changedArgs[changedArgs.length - 1] = (...nextArgs: any) => { + // If the 'next' function is called, then legit request was made. + calledNext = true; + + // Then call the original 'next' + return args[args.length - 1](...nextArgs); + }; + + // Check if we need to reset the event count (if we're past the globalDebounce time) + if (now > this.nextWindow) { + // Past the end of the last window, reset. + this.nextWindow = now + globalDebounce; + this.eventCount = 0; + } + const lastCapture = this.lastCaptured.get(lspMethod); + + const sendTelemetry = (result: Awaited>) => { + // Skip doing anything if not allowed + // We should have: + // - called the next function in the middleware (this means a request was actually sent) + // - eventcount is not over the global limit + // - elapsed time since we sent this event is greater than debounce time + if ( + this.eventName && + calledNext && + this.eventCount < globalLimit && + (!lastCapture || now - lastCapture > debounceMilliseconds) + ) { + // We're sending, so update event count and last captured time + this.lastCaptured.set(lspMethod, now); + this.eventCount += 1; + + // Replace all slashes in the method name so it doesn't get scrubbed by @vscode/extension-telemetry. + const formattedMethod = lspMethod.replace(/\//g, '.'); + + const properties = { + lsVersion: this.serverVersion || 'unknown', + method: formattedMethod, + }; + + let measures: number | Record = stopWatch.elapsedTime; + if (lazyMeasures) { + measures = { + duration: measures, + ...lazyMeasures(this, result), + }; + } + + this.sendTelemetryEventFunc(this.eventName, measures, properties); + } + return result; + }; + + // Try to call the 'next' function in the middleware chain + const result: ReturnType = this.callNext(funcName, changedArgs as any); + + // Then wait for the result before sending telemetry + if (isThenable(result)) { + return result.then(sendTelemetry); + } + return sendTelemetry(result as any) as ReturnType; + } +} diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts deleted file mode 100644 index 68becd50ea66..000000000000 --- a/src/client/activation/languageServer/activator.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; - -import { IWorkspaceService } from '../../common/application/types'; -import { EXTENSION_ROOT_DIR, isTestExecution } from '../../common/constants'; -import { IFileSystem } from '../../common/platform/types'; -import { BANNER_NAME_PROPOSE_LS, IConfigurationService, IPythonExtensionBanner, Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { LanguageServerActivatorBase } from '../common/activatorBase'; -import { ILanguageServerDownloader, ILanguageServerFolderService, ILanguageServerManager } from '../types'; - -/** - * Starts the language server managers per workspaces (currently one for first workspace). - * - * @export - * @class DotNetLanguageServerActivator - * @implements {ILanguageServerActivator} - * @extends {LanguageServerActivatorBase} - */ -@injectable() -export class DotNetLanguageServerActivator extends LanguageServerActivatorBase { - constructor( - @inject(ILanguageServerManager) manager: ILanguageServerManager, - @inject(IWorkspaceService) workspace: IWorkspaceService, - @inject(IFileSystem) fs: IFileSystem, - @inject(ILanguageServerDownloader) private readonly lsDownloader: ILanguageServerDownloader, - @inject(ILanguageServerFolderService) - private readonly languageServerFolderService: ILanguageServerFolderService, - @inject(IConfigurationService) configurationService: IConfigurationService, - @inject(IPythonExtensionBanner) - @named(BANNER_NAME_PROPOSE_LS) - private proposePylancePopup: IPythonExtensionBanner - ) { - super(manager, workspace, fs, configurationService); - } - - public async start(resource: Resource, interpreter?: PythonInterpreter): Promise { - if (!isTestExecution()) { - this.proposePylancePopup.showBanner().ignoreErrors(); - } - return super.start(resource, interpreter); - } - - public async ensureLanguageServerIsAvailable(resource: Resource): Promise { - const languageServerFolderPath = await this.ensureLanguageServerFileIsAvailable(resource, 'mscorlib.dll'); - if (languageServerFolderPath) { - await this.prepareLanguageServerForNoICU(languageServerFolderPath); - } - } - - public async prepareLanguageServerForNoICU(languageServerFolderPath: string): Promise { - const targetJsonFile = path.join( - languageServerFolderPath, - 'Microsoft.Python.LanguageServer.runtimeconfig.json' - ); - // tslint:disable-next-line:no-any - let content: any = {}; - if (await this.fs.fileExists(targetJsonFile)) { - try { - content = JSON.parse(await this.fs.readFile(targetJsonFile)); - if ( - content.runtimeOptions && - content.runtimeOptions.configProperties && - content.runtimeOptions.configProperties['System.Globalization.Invariant'] === true - ) { - return; - } - } catch { - // Do nothing. - } - } - content.runtimeOptions = content.runtimeOptions || {}; - content.runtimeOptions.configProperties = content.runtimeOptions.configProperties || {}; - content.runtimeOptions.configProperties['System.Globalization.Invariant'] = true; - await this.fs.writeFile(targetJsonFile, JSON.stringify(content)); - } - - private async ensureLanguageServerFileIsAvailable( - resource: Resource, - fileName: string - ): Promise { - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer === false) { - // Development mode - return; - } - const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - if (languageServerFolder) { - const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); - const mscorlib = path.join(languageServerFolderPath, fileName); - if (!(await this.fs.fileExists(mscorlib))) { - await this.lsDownloader.downloadLanguageServer(languageServerFolderPath, resource); - } - return languageServerFolderPath; - } - } -} diff --git a/src/client/activation/languageServer/analysisOptions.ts b/src/client/activation/languageServer/analysisOptions.ts deleted file mode 100644 index b05d604e43d5..000000000000 --- a/src/client/activation/languageServer/analysisOptions.ts +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ConfigurationChangeEvent, WorkspaceFolder } from 'vscode'; -import { DocumentFilter } from 'vscode-languageclient/node'; - -import { IWorkspaceService } from '../../common/application/types'; -import { traceDecorators, traceError } from '../../common/logger'; -import { IConfigurationService, IExtensionContext, IPathUtils, Resource } from '../../common/types'; -import { debounceSync } from '../../common/utils/decorators'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; -import { ILanguageServerFolderService, ILanguageServerOutputChannel } from '../types'; - -@injectable() -export class DotNetLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { - private resource: Resource; - private interpreter: PythonInterpreter | undefined; - private languageServerFolder: string = ''; - private excludedFiles: string[] = []; - private typeshedPaths: string[] = []; - - constructor( - @inject(IExtensionContext) private readonly context: IExtensionContext, - @inject(IEnvironmentVariablesProvider) envVarsProvider: IEnvironmentVariablesProvider, - @inject(IConfigurationService) private readonly configuration: IConfigurationService, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(ILanguageServerOutputChannel) lsOutputChannel: ILanguageServerOutputChannel, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, - @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService - ) { - super(envVarsProvider, lsOutputChannel); - } - - public async initialize(resource: Resource, interpreter: PythonInterpreter | undefined) { - await super.initialize(resource, interpreter); - - this.resource = resource; - this.interpreter = interpreter; - this.languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - - const disposable = this.workspace.onDidChangeConfiguration(this.onSettingsChangedHandler, this); - this.disposables.push(disposable); - } - - protected getWorkspaceFolder(): WorkspaceFolder | undefined { - return this.workspace.getWorkspaceFolder(this.resource); - } - - protected getDocumentFilters(workspaceFolder?: WorkspaceFolder): DocumentFilter[] { - const filters = super.getDocumentFilters(workspaceFolder); - - // Set the document selector only when in a multi-root workspace scenario. - if ( - workspaceFolder && - Array.isArray(this.workspace.workspaceFolders) && - this.workspace.workspaceFolders!.length > 1 - ) { - filters[0].pattern = `${workspaceFolder.uri.fsPath}/**/*`; - } - - return filters; - } - - protected async getInitializationOptions() { - const properties: Record = {}; - - const interpreterInfo = this.interpreter; - if (!interpreterInfo) { - throw Error('did not find an active interpreter'); - } - - properties.InterpreterPath = interpreterInfo.path; - - const version = interpreterInfo.version; - if (version) { - properties.Version = `${version.major}.${version.minor}.${version.patch}`; - } else { - traceError('Unable to determine Python version. Analysis may be limited.'); - } - - let searchPaths = []; - - const settings = this.configuration.getSettings(this.resource); - if (settings.autoComplete) { - const extraPaths = settings.autoComplete.extraPaths; - if (extraPaths && extraPaths.length > 0) { - searchPaths.push(...extraPaths); - } - } - - const envPythonPath = await this.getEnvPythonPath(); - if (envPythonPath !== '') { - const paths = envPythonPath.split(this.pathUtils.delimiter).filter((item) => item.trim().length > 0); - searchPaths.push(...paths); - } - - searchPaths = searchPaths.map((p) => path.normalize(p)); - - this.excludedFiles = this.getExcludedFiles(); - this.typeshedPaths = this.getTypeshedPaths(); - - return { - interpreter: { - properties - }, - searchPaths, - typeStubSearchPaths: this.typeshedPaths, - cacheFolderPath: this.getCacheFolderPath(), - excludeFiles: this.excludedFiles - }; - } - - protected getExcludedFiles(): string[] { - const list: string[] = ['**/Lib/**', '**/site-packages/**']; - this.getVsCodeExcludeSection('search.exclude', list); - this.getVsCodeExcludeSection('files.exclude', list); - this.getVsCodeExcludeSection('files.watcherExclude', list); - this.getPythonExcludeSection(list); - return list; - } - - protected getVsCodeExcludeSection(setting: string, list: string[]): void { - const states = this.workspace.getConfiguration(setting); - if (states) { - Object.keys(states) - .filter((k) => (k.indexOf('*') >= 0 || k.indexOf('/') >= 0) && states[k]) - .forEach((p) => list.push(p)); - } - } - - protected getPythonExcludeSection(list: string[]): void { - const pythonSettings = this.configuration.getSettings(this.resource); - const paths = pythonSettings && pythonSettings.linting ? pythonSettings.linting.ignorePatterns : undefined; - if (paths && Array.isArray(paths)) { - paths.filter((p) => p && p.length > 0).forEach((p) => list.push(p)); - } - } - - protected getTypeshedPaths(): string[] { - const settings = this.configuration.getSettings(this.resource); - return settings.analysis.typeshedPaths && settings.analysis.typeshedPaths.length > 0 - ? settings.analysis.typeshedPaths - : [path.join(this.context.extensionPath, this.languageServerFolder, 'Typeshed')]; - } - - protected getCacheFolderPath(): string | null { - const settings = this.configuration.getSettings(this.resource); - return settings.analysis.cacheFolderPath && settings.analysis.cacheFolderPath.length > 0 - ? settings.analysis.cacheFolderPath - : null; - } - - protected async onSettingsChangedHandler(e?: ConfigurationChangeEvent): Promise { - if (e && !e.affectsConfiguration('python', this.resource)) { - return; - } - this.onSettingsChanged(); - } - - @debounceSync(1000) - protected onSettingsChanged(): void { - this.notifyIfSettingsChanged().ignoreErrors(); - } - - @traceDecorators.verbose('Changes in python settings detected in analysis options') - protected async notifyIfSettingsChanged(): Promise { - const excludedFiles = this.getExcludedFiles(); - await this.notifyIfValuesHaveChanged(this.excludedFiles, excludedFiles); - - const typeshedPaths = this.getTypeshedPaths(); - await this.notifyIfValuesHaveChanged(this.typeshedPaths, typeshedPaths); - } - - protected async notifyIfValuesHaveChanged(oldArray: string[], newArray: string[]): Promise { - if (newArray.length !== oldArray.length) { - this.didChange.fire(); - return; - } - - for (let i = 0; i < oldArray.length; i += 1) { - if (oldArray[i] !== newArray[i]) { - this.didChange.fire(); - return; - } - } - } -} diff --git a/src/client/activation/languageServer/languageClientFactory.ts b/src/client/activation/languageServer/languageClientFactory.ts deleted file mode 100644 index 89b81e4f5284..000000000000 --- a/src/client/activation/languageServer/languageClientFactory.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable, unmanaged } from 'inversify'; -import * as path from 'path'; -import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; - -import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; -import { IConfigurationService, Resource } from '../../common/types'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { IEnvironmentActivationService } from '../../interpreter/activation/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { ILanguageClientFactory, ILanguageServerFolderService, IPlatformData } from '../types'; - -// tslint:disable:no-require-imports no-require-imports no-var-requires max-classes-per-file - -const dotNetCommand = 'dotnet'; -const languageClientName = 'Python Tools'; - -export class DotNetDownloadedLanguageClientFactory implements ILanguageClientFactory { - constructor( - private readonly platformData: IPlatformData, - private readonly languageServerFolderService: ILanguageServerFolderService - ) {} - - public async createLanguageClient( - resource: Resource, - _interpreter: PythonInterpreter | undefined, - clientOptions: LanguageClientOptions, - env?: NodeJS.ProcessEnv - ): Promise { - const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const serverModule = path.join( - EXTENSION_ROOT_DIR, - languageServerFolder, - this.platformData.engineExecutableName - ); - const options = { stdio: 'pipe', env }; - const serverOptions: ServerOptions = { - run: { command: serverModule, args: [], options }, - debug: { command: serverModule, args: ['--debug'], options } - }; - const vscodeLanguageClient = require('vscode-languageclient/node') as typeof import('vscode-languageclient/node'); - return new vscodeLanguageClient.LanguageClient( - PYTHON_LANGUAGE, - languageClientName, - serverOptions, - clientOptions - ); - } -} - -export class DotNetSimpleLanguageClientFactory implements ILanguageClientFactory { - constructor( - private readonly platformData: IPlatformData, - private readonly languageServerFolderService: ILanguageServerFolderService - ) {} - - public async createLanguageClient( - resource: Resource, - _interpreter: PythonInterpreter | undefined, - clientOptions: LanguageClientOptions, - env?: NodeJS.ProcessEnv - ): Promise { - const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const options = { stdio: 'pipe', env }; - const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, this.platformData.engineDllName); - const serverOptions: ServerOptions = { - run: { command: dotNetCommand, args: [serverModule], options }, - debug: { command: dotNetCommand, args: [serverModule, '--debug'], options } - }; - const vscodeLanguageClient = require('vscode-languageclient/node') as typeof import('vscode-languageclient/node'); - return new vscodeLanguageClient.LanguageClient( - PYTHON_LANGUAGE, - languageClientName, - serverOptions, - clientOptions - ); - } -} - -@injectable() -export class DotNetLanguageClientFactory implements ILanguageClientFactory { - constructor( - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider, - @inject(IEnvironmentActivationService) - private readonly environmentActivationService: IEnvironmentActivationService, - @inject(IPlatformData) private readonly platformData: IPlatformData, - @inject(ILanguageServerFolderService) - private readonly languageServerFolderService: ILanguageServerFolderService, - @unmanaged() private readonly downloadedFactory: ILanguageClientFactory, - @unmanaged() private readonly simpleFactory: ILanguageClientFactory - ) {} - - public async createLanguageClient( - resource: Resource, - interpreter: PythonInterpreter | undefined, - clientOptions: LanguageClientOptions - ): Promise { - const settings = this.configurationService.getSettings(resource); - let factory: ILanguageClientFactory; - if (this.platformData && this.languageServerFolderService) { - factory = settings.downloadLanguageServer - ? new DotNetDownloadedLanguageClientFactory(this.platformData, this.languageServerFolderService) - : new DotNetSimpleLanguageClientFactory(this.platformData, this.languageServerFolderService); - } else { - factory = settings.downloadLanguageServer ? this.downloadedFactory : this.simpleFactory; - } - const env = await this.getEnvVars(resource, interpreter); - return factory.createLanguageClient(resource, interpreter, clientOptions, env); - } - - private async getEnvVars( - resource: Resource, - interpreter: PythonInterpreter | undefined - ): Promise { - const envVars = await this.environmentActivationService.getActivatedEnvironmentVariables(resource, interpreter); - if (envVars && Object.keys(envVars).length > 0) { - return envVars; - } - return this.envVarsProvider.getEnvironmentVariables(resource); - } -} diff --git a/src/client/activation/languageServer/languageServerCompatibilityService.ts b/src/client/activation/languageServer/languageServerCompatibilityService.ts deleted file mode 100644 index 48c1e0729595..000000000000 --- a/src/client/activation/languageServer/languageServerCompatibilityService.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IDotNetCompatibilityService } from '../../common/dotnet/types'; -import { traceError } from '../../common/logger'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { ILanguageServerCompatibilityService } from '../types'; - -@injectable() -export class LanguageServerCompatibilityService implements ILanguageServerCompatibilityService { - constructor( - @inject(IDotNetCompatibilityService) private readonly dotnetCompatibility: IDotNetCompatibilityService - ) {} - public async isSupported(): Promise { - try { - const supported = await this.dotnetCompatibility.isSupported(); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { - supported: supported - }); - return supported; - } catch (ex) { - traceError('Unable to determine whether LS is supported', ex); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { - supported: false, - failureType: 'UnknownError' - }); - return false; - } - } -} diff --git a/src/client/activation/languageServer/languageServerExtension.ts b/src/client/activation/languageServer/languageServerExtension.ts deleted file mode 100644 index b2f69abadbee..000000000000 --- a/src/client/activation/languageServer/languageServerExtension.ts +++ /dev/null @@ -1,38 +0,0 @@ -// 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 { ICommandManager } from '../../common/application/types'; -import '../../common/extensions'; -import { IDisposable } from '../../common/types'; -import { ILanguageServerExtension } from '../types'; - -const loadExtensionCommand = 'python._loadLanguageServerExtension'; - -@injectable() -export class LanguageServerExtension implements ILanguageServerExtension { - public loadExtensionArgs?: {}; - protected readonly _invoked = new EventEmitter(); - private disposable?: IDisposable; - constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager) {} - public dispose() { - if (this.disposable) { - this.disposable.dispose(); - } - } - public async register(): Promise { - if (this.disposable) { - return; - } - this.disposable = this.commandManager.registerCommand(loadExtensionCommand, (args) => { - this.loadExtensionArgs = args; - this._invoked.fire(); - }); - } - public get invoked(): Event { - return this._invoked.event; - } -} diff --git a/src/client/activation/languageServer/languageServerFolderService.ts b/src/client/activation/languageServer/languageServerFolderService.ts deleted file mode 100644 index bf541ab4690f..000000000000 --- a/src/client/activation/languageServer/languageServerFolderService.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IApplicationEnvironment } from '../../common/application/types'; -import { IServiceContainer } from '../../ioc/types'; -import { LanguageServerFolderService } from '../common/languageServerFolderService'; -import { DotNetLanguageServerFolder } from '../types'; - -// Must match languageServerVersion* keys in package.json -export const DotNetLanguageServerMinVersionKey = 'languageServerVersion'; - -@injectable() -export class DotNetLanguageServerFolderService extends LanguageServerFolderService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, DotNetLanguageServerFolder); - } - - protected getMinimalLanguageServerVersion(): string { - let minVersion = '0.0.0'; - try { - const appEnv = this.serviceContainer.get(IApplicationEnvironment); - if (appEnv) { - minVersion = appEnv.packageJson[DotNetLanguageServerMinVersionKey] as string; - } - // tslint:disable-next-line: no-empty - } catch {} - return minVersion; - } -} diff --git a/src/client/activation/languageServer/languageServerPackageRepository.ts b/src/client/activation/languageServer/languageServerPackageRepository.ts deleted file mode 100644 index b9c8f3da797e..000000000000 --- a/src/client/activation/languageServer/languageServerPackageRepository.ts +++ /dev/null @@ -1,35 +0,0 @@ -// 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 { - BetaLanguageServerPackageRepository, - DailyLanguageServerPackageRepository, - StableLanguageServerPackageRepository -} from '../common/packageRepository'; - -const languageServerPackageName = 'python-language-server'; - -@injectable() -export class StableDotNetLanguageServerPackageRepository extends StableLanguageServerPackageRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, languageServerPackageName); - } -} - -@injectable() -export class BetaDotNetLanguageServerPackageRepository extends BetaLanguageServerPackageRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, languageServerPackageName); - } -} - -@injectable() -export class DailyDotNetLanguageServerPackageRepository extends DailyLanguageServerPackageRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, languageServerPackageName); - } -} diff --git a/src/client/activation/languageServer/languageServerPackageService.ts b/src/client/activation/languageServer/languageServerPackageService.ts deleted file mode 100644 index 9a912645fb4f..000000000000 --- a/src/client/activation/languageServer/languageServerPackageService.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IApplicationEnvironment } from '../../common/application/types'; -import { IPlatformService } from '../../common/platform/types'; -import { OSType } from '../../common/utils/platform'; -import { IServiceContainer } from '../../ioc/types'; -import { LanguageServerPackageService } from '../common/languageServerPackageService'; -import { PlatformName } from '../types'; - -const downloadBaseFileName = 'Python-Language-Server'; -export const PackageNames = { - [PlatformName.Windows32Bit]: `${downloadBaseFileName}-${PlatformName.Windows32Bit}`, - [PlatformName.Windows64Bit]: `${downloadBaseFileName}-${PlatformName.Windows64Bit}`, - [PlatformName.Linux64Bit]: `${downloadBaseFileName}-${PlatformName.Linux64Bit}`, - [PlatformName.Mac64Bit]: `${downloadBaseFileName}-${PlatformName.Mac64Bit}` -}; - -@injectable() -export class DotNetLanguageServerPackageService extends LanguageServerPackageService { - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IApplicationEnvironment) appEnv: IApplicationEnvironment, - @inject(IPlatformService) platform: IPlatformService - ) { - super(serviceContainer, appEnv, platform); - } - - public getNugetPackageName(): string { - switch (this.platform.osType) { - case OSType.Windows: - return PackageNames[this.platform.is64bit ? PlatformName.Windows64Bit : PlatformName.Windows32Bit]; - case OSType.OSX: - return PackageNames[PlatformName.Mac64Bit]; - default: - return PackageNames[PlatformName.Linux64Bit]; - } - } -} diff --git a/src/client/activation/languageServer/languageServerProxy.ts b/src/client/activation/languageServer/languageServerProxy.ts deleted file mode 100644 index bc89929cfa38..000000000000 --- a/src/client/activation/languageServer/languageServerProxy.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; - -import { traceDecorators, traceError } from '../../common/logger'; -import { IConfigurationService, Resource } from '../../common/types'; -import { createDeferred, Deferred, sleep } from '../../common/utils/async'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { noop } from '../../common/utils/misc'; -import { LanguageServerSymbolProvider } from '../../providers/symbolProvider'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { ITestManagementService } from '../../testing/types'; -import { ProgressReporting } from '../progress'; -import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; - -@injectable() -export class DotNetLanguageServerProxy implements ILanguageServerProxy { - public languageClient: LanguageClient | undefined; - private startupCompleted: Deferred; - private readonly disposables: Disposable[] = []; - private extensionLoadedArgs = new Set<{}>(); - private disposed: boolean = false; - - constructor( - @inject(ILanguageClientFactory) private readonly factory: ILanguageClientFactory, - @inject(ITestManagementService) private readonly testManager: ITestManagementService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService - ) { - this.startupCompleted = createDeferred(); - } - @traceDecorators.verbose('Stopping language server') - public dispose() { - if (this.languageClient) { - // Do not await on this. - this.languageClient.stop().then(noop, (ex) => traceError('Stopping language client failed', ex)); - this.languageClient = undefined; - } - while (this.disposables.length > 0) { - const d = this.disposables.shift()!; - d.dispose(); - } - if (this.startupCompleted.completed) { - this.startupCompleted.reject(new Error('Disposed language server')); - this.startupCompleted = createDeferred(); - } - this.disposed = true; - } - - @traceDecorators.error('Failed to start language server') - @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_ENABLED, undefined, true) - public async start( - resource: Resource, - interpreter: PythonInterpreter | undefined, - options: LanguageClientOptions - ): Promise { - if (!this.languageClient) { - this.languageClient = await this.factory.createLanguageClient(resource, interpreter, options); - this.disposables.push(this.languageClient!.start()); - await this.serverReady(); - if (this.disposed) { - // Check if it got disposed in the interim. - return; - } - const progressReporting = new ProgressReporting(this.languageClient!); - this.disposables.push(progressReporting); - - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer) { - this.languageClient.onTelemetry((telemetryEvent) => { - const eventName = telemetryEvent.EventName || EventName.PYTHON_LANGUAGE_SERVER_TELEMETRY; - const formattedProperties = { - ...telemetryEvent.Properties, - // Replace all slashes in the method name so it doesn't get scrubbed by vscode-extension-telemetry. - method: telemetryEvent.Properties.method?.replace(/\//g, '.') - }; - sendTelemetryEvent(eventName, telemetryEvent.Measurements, formattedProperties); - }); - } - await this.registerTestServices(); - } else { - await this.startupCompleted.promise; - } - } - @traceDecorators.error('Failed to load language server extension') - public loadExtension(args?: {}) { - if (this.extensionLoadedArgs.has(args || '')) { - return; - } - this.extensionLoadedArgs.add(args || ''); - this.startupCompleted.promise - .then(() => - this.languageClient!.sendRequest('python/loadExtension', args).then(noop, (ex) => - traceError('Request python/loadExtension failed', ex) - ) - ) - .ignoreErrors(); - } - @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_READY, undefined, true) - protected async serverReady(): Promise { - // languageClient can be disposed in awaits. - while (this.languageClient && !this.languageClient.initializeResult) { - await sleep(100); - } - if (this.languageClient) { - await this.languageClient.onReady(); - } - this.startupCompleted.resolve(); - } - @swallowExceptions('Activating Unit Tests Manager for Microsoft Python Language Server') - protected async registerTestServices() { - if (!this.languageClient) { - throw new Error('languageClient not initialized'); - } - await this.testManager.activate(new LanguageServerSymbolProvider(this.languageClient!)); - } -} diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts deleted file mode 100644 index 0d0703541b9b..000000000000 --- a/src/client/activation/languageServer/manager.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import '../../common/extensions'; - -import { inject, injectable, named } from 'inversify'; - -import { ICommandManager } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; -import { IConfigurationService, IDisposable, IExperimentsManager, Resource } from '../../common/types'; -import { debounceSync } from '../../common/utils/decorators'; -import { IServiceContainer } from '../../ioc/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { Commands } from '../commands'; -import { LanguageClientMiddleware } from '../languageClientMiddleware'; -import { - ILanguageServerAnalysisOptions, - ILanguageServerExtension, - ILanguageServerFolderService, - ILanguageServerManager, - ILanguageServerProxy, - LanguageServerType -} from '../types'; - -@injectable() -export class DotNetLanguageServerManager implements ILanguageServerManager { - private languageServerProxy?: ILanguageServerProxy; - private resource!: Resource; - private interpreter: PythonInterpreter | undefined; - private middleware: LanguageClientMiddleware | undefined; - private disposables: IDisposable[] = []; - private connected: boolean = false; - private lsVersion: string | undefined; - - constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ILanguageServerAnalysisOptions) - @named(LanguageServerType.Microsoft) - private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ILanguageServerExtension) private readonly lsExtension: ILanguageServerExtension, - @inject(ILanguageServerFolderService) private readonly folderService: ILanguageServerFolderService, - @inject(IExperimentsManager) private readonly experimentsManager: IExperimentsManager, - @inject(IConfigurationService) private readonly configService: IConfigurationService, - @inject(ICommandManager) commandManager: ICommandManager - ) { - this.disposables.push( - commandManager.registerCommand(Commands.RestartLS, () => { - this.restartLanguageServer().ignoreErrors(); - }) - ); - } - - private static versionTelemetryProps(instance: DotNetLanguageServerManager) { - return { - lsVersion: instance.lsVersion - }; - } - - public dispose() { - if (this.languageProxy) { - this.languageProxy.dispose(); - } - this.disposables.forEach((d) => d.dispose()); - } - - public get languageProxy() { - return this.languageServerProxy; - } - @traceDecorators.error('Failed to start language server') - public async start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise { - if (this.languageProxy) { - throw new Error('Language server already started'); - } - this.registerCommandHandler(); - this.resource = resource; - this.interpreter = interpreter; - this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); - - const versionPair = await this.folderService.getCurrentLanguageServerDirectory(); - this.lsVersion = versionPair?.version.format(); - - await this.analysisOptions.initialize(resource, interpreter); - await this.startLanguageServer(); - } - public connect() { - this.connected = true; - this.middleware?.connect(); - } - public disconnect() { - this.connected = false; - this.middleware?.disconnect(); - } - protected registerCommandHandler() { - this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables); - } - protected loadExtensionIfNecessary() { - if (this.languageProxy && this.lsExtension.loadExtensionArgs) { - this.languageProxy.loadExtension(this.lsExtension.loadExtensionArgs); - } - } - @debounceSync(1000) - protected restartLanguageServerDebounced(): void { - this.restartLanguageServer().ignoreErrors(); - } - @traceDecorators.error('Failed to restart language server') - @traceDecorators.verbose('Restarting language server') - protected async restartLanguageServer(): Promise { - if (this.languageProxy) { - this.languageProxy.dispose(); - } - await this.startLanguageServer(); - } - @captureTelemetry( - EventName.PYTHON_LANGUAGE_SERVER_STARTUP, - undefined, - true, - undefined, - DotNetLanguageServerManager.versionTelemetryProps - ) - @traceDecorators.verbose('Starting language server') - protected async startLanguageServer(): Promise { - this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); - - const options = await this.analysisOptions!.getAnalysisOptions(); - options.middleware = this.middleware = new LanguageClientMiddleware( - this.experimentsManager, - this.configService, - LanguageServerType.Microsoft, - this.lsVersion - ); - - // Make sure the middleware is connected if we restart and we we're already connected. - if (this.connected) { - this.middleware.connect(); - } - - // Then use this middleware to start a new language client. - await this.languageServerProxy.start(this.resource, this.interpreter, options); - this.loadExtensionIfNecessary(); - } -} diff --git a/src/client/activation/languageServer/outputChannel.ts b/src/client/activation/languageServer/outputChannel.ts deleted file mode 100644 index 2d4815b3af97..000000000000 --- a/src/client/activation/languageServer/outputChannel.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 '../../common/extensions'; -import { IOutputChannel } from '../../common/types'; -import { OutputChannelNames } from '../../common/utils/localize'; -import { ILanguageServerOutputChannel } from '../types'; - -@injectable() -export class LanguageServerOutputChannel implements ILanguageServerOutputChannel { - public output: IOutputChannel | undefined; - private registered = false; - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(ICommandManager) private readonly commandManager: ICommandManager - ) {} - - public get channel() { - if (!this.output) { - this.output = this.appShell.createOutputChannel(OutputChannelNames.languageServer()); - this.registerCommand().ignoreErrors(); - } - return this.output; - } - private async registerCommand() { - if (this.registered) { - return; - } - this.registered = true; - // This controls the visibility of the command used to display the LS Output panel. - // We don't want to display it when Jedi is used instead of LS. - await this.commandManager.executeCommand('setContext', 'python.hasLanguageServerOutputChannel', true); - this.commandManager.registerCommand('python.viewLanguageServerOutput', () => this.output!.show(true)); - } -} diff --git a/src/client/activation/languageServer/platformData.ts b/src/client/activation/languageServer/platformData.ts deleted file mode 100644 index f616be1fc764..000000000000 --- a/src/client/activation/languageServer/platformData.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 { IPlatformData } from '../types'; - -export enum PlatformName { - Windows32Bit = 'win-x86', - Windows64Bit = 'win-x64', - Mac64Bit = 'osx-x64', - Linux64Bit = 'linux-x64' -} - -export enum PlatformLSExecutables { - Windows = 'Microsoft.Python.LanguageServer.exe', - MacOS = 'Microsoft.Python.LanguageServer', - Linux = 'Microsoft.Python.LanguageServer' -} - -@injectable() -export class PlatformData implements IPlatformData { - constructor(@inject(IPlatformService) private readonly platform: IPlatformService) {} - public get platformName(): PlatformName { - if (this.platform.isWindows) { - return this.platform.is64bit ? PlatformName.Windows64Bit : PlatformName.Windows32Bit; - } - if (this.platform.isMac) { - return PlatformName.Mac64Bit; - } - if (this.platform.isLinux) { - if (!this.platform.is64bit) { - throw new Error('Microsoft Python Language Server does not support 32-bit Linux.'); - } - return PlatformName.Linux64Bit; - } - throw new Error('Unknown OS platform.'); - } - - public get engineDllName(): string { - return 'Microsoft.Python.LanguageServer.dll'; - } - - public get engineExecutableName(): string { - if (this.platform.isWindows) { - return PlatformLSExecutables.Windows; - } else if (this.platform.isLinux) { - return PlatformLSExecutables.Linux; - } else if (this.platform.isMac) { - return PlatformLSExecutables.MacOS; - } else { - return 'unknown-platform'; - } - } -} diff --git a/src/client/activation/node/activator.ts b/src/client/activation/node/activator.ts deleted file mode 100644 index fc438bece25b..000000000000 --- a/src/client/activation/node/activator.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { CancellationToken, CompletionItem, ProviderResult } from 'vscode'; -// tslint:disable-next-line: import-name -import ProtocolCompletionItem from 'vscode-languageclient/lib/common/protocolCompletionItem'; -import { CompletionResolveRequest } from 'vscode-languageclient/node'; -import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../common/application/types'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IExtensions, Resource } from '../../common/types'; -import { Pylance } from '../../common/utils/localize'; -import { LanguageServerActivatorBase } from '../common/activatorBase'; -import { promptForPylanceInstall } from '../common/languageServerChangeHandler'; -import { ILanguageServerManager } from '../types'; - -/** - * Starts Pylance language server manager. - * - * @export - * @class NodeLanguageServerActivator - * @implements {ILanguageServerActivator} - * @extends {LanguageServerActivatorBase} - */ -@injectable() -export class NodeLanguageServerActivator extends LanguageServerActivatorBase { - constructor( - @inject(ILanguageServerManager) manager: ILanguageServerManager, - @inject(IWorkspaceService) workspace: IWorkspaceService, - @inject(IFileSystem) fs: IFileSystem, - @inject(IConfigurationService) configurationService: IConfigurationService, - @inject(IExtensions) private readonly extensions: IExtensions, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IApplicationEnvironment) private readonly appEnv: IApplicationEnvironment - ) { - super(manager, workspace, fs, configurationService); - } - - public async ensureLanguageServerIsAvailable(resource: Resource): Promise { - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer === false) { - // Development mode. - return; - } - if (!this.extensions.getExtension(PYLANCE_EXTENSION_ID)) { - // Pylance is not yet installed. Throw will cause activator to use Jedi - // temporarily. Language server installation tracker will prompt for window - // reload when Pylance becomes available. - await promptForPylanceInstall(this.appShell, this.appEnv); - throw new Error(Pylance.pylanceNotInstalledMessage()); - } - } - - public resolveCompletionItem(item: CompletionItem, token: CancellationToken): ProviderResult { - return this.handleResolveCompletionItem(item, token); - } - - private async handleResolveCompletionItem( - item: CompletionItem, - token: CancellationToken - ): Promise { - const languageClient = this.getLanguageClient(); - - if (languageClient) { - // Turn our item into a ProtocolCompletionItem before we convert it. This preserves the .data - // attribute that it has and is needed to match on the language server side. - const protoItem: ProtocolCompletionItem = new ProtocolCompletionItem(item.label); - Object.assign(protoItem, item); - - const args = languageClient.code2ProtocolConverter.asCompletionItem(protoItem); - const result = await languageClient.sendRequest(CompletionResolveRequest.type, args, token); - - if (result) { - return languageClient.protocol2CodeConverter.asCompletionItem(result); - } - } - } -} diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index 487ecc7e2208..71295649c25a 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -1,17 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { IWorkspaceService } from '../../common/application/types'; + import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; -@injectable() export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { - constructor( - @inject(IEnvironmentVariablesProvider) envVarsProvider: IEnvironmentVariablesProvider, - @inject(ILanguageServerOutputChannel) lsOutputChannel: ILanguageServerOutputChannel - ) { - super(envVarsProvider, lsOutputChannel); + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(lsOutputChannel: ILanguageServerOutputChannel, workspace: IWorkspaceService) { + super(lsOutputChannel, workspace); + } + + protected getConfigSectionsToSynchronize(): string[] { + return [...super.getConfigSectionsToSynchronize(), 'jupyter.runStartupCommands']; + } + + // eslint-disable-next-line class-methods-use-this + protected async getInitializationOptions(): Promise { + return ({ + experimentationSupport: true, + trustedWorkspaceSupport: true, + } as unknown) as LanguageClientOptions; } } diff --git a/src/client/activation/node/languageClientFactory.ts b/src/client/activation/node/languageClientFactory.ts index 52f0320cabc9..9543f265468f 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/languageClientFactory.ts @@ -1,43 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; import * as path from 'path'; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node'; -import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; +import { PYLANCE_EXTENSION_ID, PYTHON_LANGUAGE } from '../../common/constants'; import { IFileSystem } from '../../common/platform/types'; -import { Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { ILanguageClientFactory, ILanguageServerFolderService } from '../types'; -import { FileBasedCancellationStrategy } from './cancellationUtils'; +import { IExtensions, Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; +import { ILanguageClientFactory } from '../types'; -// tslint:disable:no-require-imports no-require-imports no-var-requires max-classes-per-file -const languageClientName = 'Python Tools'; +export const PYLANCE_NAME = 'Pylance'; -@injectable() export class NodeLanguageClientFactory implements ILanguageClientFactory { - constructor( - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService - ) {} + constructor(private readonly fs: IFileSystem, private readonly extensions: IExtensions) {} public async createLanguageClient( - resource: Resource, - _interpreter: PythonInterpreter | undefined, - clientOptions: LanguageClientOptions + _resource: Resource, + _interpreter: PythonEnvironment | undefined, + clientOptions: LanguageClientOptions, ): Promise { // this must exist for node language client const commandArgs = (clientOptions.connectionOptions ?.cancellationStrategy as FileBasedCancellationStrategy).getCommandLineArguments(); - const folderName = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const languageServerFolder = path.isAbsolute(folderName) - ? folderName - : path.join(EXTENSION_ROOT_DIR, folderName); - - const bundlePath = path.join(languageServerFolder, 'server.bundle.js'); - const nonBundlePath = path.join(languageServerFolder, 'server.js'); + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + const languageServerFolder = extension ? extension.extensionPath : ''; + const bundlePath = path.join(languageServerFolder, 'dist', 'server.bundle.js'); + const nonBundlePath = path.join(languageServerFolder, 'dist', 'server.js'); const modulePath = (await this.fs.fileExists(nonBundlePath)) ? nonBundlePath : bundlePath; const debugOptions = { execArgv: ['--nolazy', '--inspect=6600'] }; @@ -46,7 +37,7 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory { run: { module: bundlePath, transport: TransportKind.ipc, - args: commandArgs + args: commandArgs, }, // In debug mode, use the non-bundled code if it's present. The production // build includes only the bundled package, so we don't want to crash if @@ -55,16 +46,10 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory { module: modulePath, transport: TransportKind.ipc, options: debugOptions, - args: commandArgs - } + args: commandArgs, + }, }; - const vscodeLanguageClient = require('vscode-languageclient/node') as typeof import('vscode-languageclient/node'); - return new vscodeLanguageClient.LanguageClient( - PYTHON_LANGUAGE, - languageClientName, - serverOptions, - clientOptions - ); + return new LanguageClient(PYTHON_LANGUAGE, PYLANCE_NAME, serverOptions, clientOptions); } } diff --git a/src/client/activation/node/languageClientMiddleware.ts b/src/client/activation/node/languageClientMiddleware.ts new file mode 100644 index 000000000000..dfd65f1bb418 --- /dev/null +++ b/src/client/activation/node/languageClientMiddleware.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceContainer } from '../../ioc/types'; +import { LanguageClientMiddleware } from '../languageClientMiddleware'; + +import { LanguageServerType } from '../types'; + +export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { + public constructor(serviceContainer: IServiceContainer, serverVersion?: string) { + super(serviceContainer, LanguageServerType.Node, serverVersion); + } +} diff --git a/src/client/activation/node/languageServerFolderService.ts b/src/client/activation/node/languageServerFolderService.ts deleted file mode 100644 index 0ccb89b28fc3..000000000000 --- a/src/client/activation/node/languageServerFolderService.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { IWorkspaceService } from '../../common/application/types'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { NugetPackage } from '../../common/nuget/types'; -import { IConfigurationService, IExtensions, Resource } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { LanguageServerFolderService } from '../common/languageServerFolderService'; -import { FolderVersionPair, ILanguageServerFolderService, NodeLanguageServerFolder } from '../types'; - -class FallbackNodeLanguageServerFolderService extends LanguageServerFolderService { - constructor(serviceContainer: IServiceContainer) { - super(serviceContainer, NodeLanguageServerFolder); - } - - protected getMinimalLanguageServerVersion(): string { - return '0.0.0'; - } -} - -// Exported for testing. -export interface ILanguageServerFolder { - path: string; - version: string; // SemVer, in string form to avoid cross-extension type issues. -} - -// Exported for testing. -export interface ILSExtensionApi { - languageServerFolder?(): Promise; -} - -@injectable() -export class NodeLanguageServerFolderService implements ILanguageServerFolderService { - private readonly fallback: FallbackNodeLanguageServerFolderService; - - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IExtensions) readonly extensions: IExtensions - ) { - this.fallback = new FallbackNodeLanguageServerFolderService(serviceContainer); - } - - public async skipDownload(): Promise { - return (await this.lsExtensionApi()) !== undefined; - } - - public async getLanguageServerFolderName(resource: Resource): Promise { - const lsf = await this.languageServerFolder(); - if (lsf) { - assert.ok(path.isAbsolute(lsf.path)); - return lsf.path; - } - return this.fallback.getLanguageServerFolderName(resource); - } - - public async getLatestLanguageServerVersion(resource: Resource): Promise { - if (await this.lsExtensionApi()) { - return undefined; - } - return this.fallback.getLatestLanguageServerVersion(resource); - } - - public async getCurrentLanguageServerDirectory(): Promise { - const lsf = await this.languageServerFolder(); - if (lsf) { - assert.ok(path.isAbsolute(lsf.path)); - return { - path: lsf.path, - version: new SemVer(lsf.version) - }; - } - return this.fallback.getCurrentLanguageServerDirectory(); - } - - protected async languageServerFolder(): Promise { - const extension = await this.lsExtensionApi(); - if (!extension?.languageServerFolder) { - return undefined; - } - return extension.languageServerFolder(); - } - - private async lsExtensionApi(): Promise { - // downloadLanguageServer is a bit of a misnomer; if false then this indicates that a local - // development copy should be run instead of a "real" build, telemetry discarded, etc. - // So, we require it to be true, even though in the pinned case no real download happens. - if ( - !this.configService.getSettings().downloadLanguageServer || - this.workspaceService.getConfiguration('python').get('packageName') - ) { - return undefined; - } - - const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); - if (!extension) { - return undefined; - } - - if (!extension.isActive) { - return extension.activate(); - } - - return extension.exports; - } -} diff --git a/src/client/activation/node/languageServerPackageRepository.ts b/src/client/activation/node/languageServerPackageRepository.ts deleted file mode 100644 index 39ab7ac86a78..000000000000 --- a/src/client/activation/node/languageServerPackageRepository.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 { IServiceContainer } from '../../ioc/types'; -import { - BetaLanguageServerPackageRepository, - DailyLanguageServerPackageRepository, - StableLanguageServerPackageRepository -} from '../common/packageRepository'; - -@injectable() -export class StableNodeLanguageServerPackageRepository extends StableLanguageServerPackageRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - const config = serviceContainer.get(IWorkspaceService).getConfiguration('python'); - const packageName = config.get('blobName') || ''; - super(serviceContainer, packageName); - } -} - -@injectable() -export class BetaNodeLanguageServerPackageRepository extends BetaLanguageServerPackageRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - const config = serviceContainer.get(IWorkspaceService).getConfiguration('python'); - const packageName = config.get('blobName') || ''; - super(serviceContainer, packageName); - } -} - -@injectable() -export class DailyNodeLanguageServerPackageRepository extends DailyLanguageServerPackageRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - const config = serviceContainer.get(IWorkspaceService).getConfiguration('python'); - const packageName = config.get('blobName') || ''; - super(serviceContainer, packageName); - } -} diff --git a/src/client/activation/node/languageServerPackageService.ts b/src/client/activation/node/languageServerPackageService.ts deleted file mode 100644 index feacaba70a8a..000000000000 --- a/src/client/activation/node/languageServerPackageService.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; -import { IPlatformService } from '../../common/platform/types'; -import { IServiceContainer } from '../../ioc/types'; -import { LanguageServerPackageService } from '../common/languageServerPackageService'; - -@injectable() -export class NodeLanguageServerPackageService extends LanguageServerPackageService { - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IApplicationEnvironment) appEnv: IApplicationEnvironment, - @inject(IPlatformService) platform: IPlatformService - ) { - super(serviceContainer, appEnv, platform); - } - - public getNugetPackageName(): string { - const config = this.serviceContainer.get(IWorkspaceService).getConfiguration('python'); - return config.get('packageName') || ''; - } -} diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts index 88ec6661db2c..45d1d1a17fee 100644 --- a/src/client/activation/node/languageServerProxy.ts +++ b/src/client/activation/node/languageServerProxy.ts @@ -2,164 +2,235 @@ // Licensed under the MIT License. import '../../common/extensions'; -import { inject, injectable } from 'inversify'; import { DidChangeConfigurationNotification, Disposable, LanguageClient, - LanguageClientOptions + LanguageClientOptions, } from 'vscode-languageclient/node'; -import { DeprecatePythonPath } from '../../common/experiments/groups'; -import { traceDecorators, traceError } from '../../common/logger'; -import { IConfigurationService, IExperimentsManager, IInterpreterPathService, Resource } from '../../common/types'; -import { createDeferred, Deferred, sleep } from '../../common/utils/async'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { noop } from '../../common/utils/misc'; -import { LanguageServerSymbolProvider } from '../../providers/symbolProvider'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { Extension } from 'vscode'; +import { IExperimentService, IExtensions, IInterpreterPathService, Resource } from '../../common/types'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { ITestManagementService } from '../../testing/types'; +import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; import { ProgressReporting } from '../progress'; -import { ILanguageClientFactory, ILanguageServerFolderService, ILanguageServerProxy } from '../types'; -import { FileBasedCancellationStrategy } from './cancellationUtils'; +import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; +import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; +import { IWorkspaceService } from '../../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { PylanceApi } from './pylanceApi'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace InExperiment { + export const Method = 'python/inExperiment'; + + export interface IRequest { + experimentName: string; + } + + export interface IResponse { + inExperiment: boolean; + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace GetExperimentValue { + export const Method = 'python/getExperimentValue'; + + export interface IRequest { + experimentName: string; + } + + export interface IResponse { + value: T | undefined; + } +} -@injectable() export class NodeLanguageServerProxy implements ILanguageServerProxy { public languageClient: LanguageClient | undefined; - private startupCompleted: Deferred; + private cancellationStrategy: FileBasedCancellationStrategy | undefined; + private readonly disposables: Disposable[] = []; - private disposed: boolean = false; + private lsVersion: string | undefined; + private pylanceApi: PylanceApi | undefined; + constructor( - @inject(ILanguageClientFactory) private readonly factory: ILanguageClientFactory, - @inject(ITestManagementService) private readonly testManager: ITestManagementService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(ILanguageServerFolderService) private readonly folderService: ILanguageServerFolderService, - @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, - @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService - ) { - this.startupCompleted = createDeferred(); - } + private readonly factory: ILanguageClientFactory, + private readonly experimentService: IExperimentService, + private readonly interpreterPathService: IInterpreterPathService, + private readonly environmentService: IEnvironmentVariablesProvider, + private readonly workspace: IWorkspaceService, + private readonly extensions: IExtensions, + ) {} private static versionTelemetryProps(instance: NodeLanguageServerProxy) { return { - lsVersion: instance.lsVersion + lsVersion: instance.lsVersion, }; } - @traceDecorators.verbose('Stopping language server') - public dispose() { - if (this.languageClient) { - // Do not await on this. - this.languageClient.stop().then(noop, (ex) => traceError('Stopping language client failed', ex)); - this.languageClient = undefined; - } - if (this.cancellationStrategy) { - this.cancellationStrategy.dispose(); - this.cancellationStrategy = undefined; - } - while (this.disposables.length > 0) { - const d = this.disposables.shift()!; - d.dispose(); - } - if (this.startupCompleted.completed) { - this.startupCompleted.reject(new Error('Disposed language server')); - this.startupCompleted = createDeferred(); - } - this.disposed = true; + @traceDecoratorVerbose('Disposing language server') + public dispose(): void { + this.stop().ignoreErrors(); } - @traceDecorators.error('Failed to start language server') + @traceDecoratorError('Failed to start language server') @captureTelemetry( EventName.LANGUAGE_SERVER_ENABLED, undefined, true, undefined, - NodeLanguageServerProxy.versionTelemetryProps + NodeLanguageServerProxy.versionTelemetryProps, ) public async start( resource: Resource, - interpreter: PythonInterpreter | undefined, - options: LanguageClientOptions + interpreter: PythonEnvironment | undefined, + options: LanguageClientOptions, ): Promise { - if (!this.languageClient) { - const directory = await this.folderService.getCurrentLanguageServerDirectory(); - this.lsVersion = directory?.version.format(); - - this.cancellationStrategy = new FileBasedCancellationStrategy(); - options.connectionOptions = { cancellationStrategy: this.cancellationStrategy }; - - this.languageClient = await this.factory.createLanguageClient(resource, interpreter, options); - this.disposables.push(this.languageClient!.start()); - await this.serverReady(); - if (this.disposed) { - // Check if it got disposed in the interim. - return; - } - const progressReporting = new ProgressReporting(this.languageClient!); - this.disposables.push(progressReporting); - - if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) { - this.disposables.push( - this.interpreterPathService.onDidChange(() => { - // Manually send didChangeConfiguration in order to get the server to requery - // the workspace configurations (to then pick up pythonPath set in the middleware). - // This is needed as interpreter changes via the interpreter path service happen - // outside of VS Code's settings (which would mean VS Code sends the config updates itself). - this.languageClient!.sendNotification(DidChangeConfigurationNotification.type, { - settings: null - }); - }) - ); - } + const extension = await this.getPylanceExtension(); + this.lsVersion = extension?.packageJSON.version || '0'; - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer) { - this.languageClient.onTelemetry((telemetryEvent) => { - const eventName = telemetryEvent.EventName || EventName.LANGUAGE_SERVER_TELEMETRY; - const formattedProperties = { - ...telemetryEvent.Properties, - // Replace all slashes in the method name so it doesn't get scrubbed by vscode-extension-telemetry. - method: telemetryEvent.Properties.method?.replace(/\//g, '.') - }; - sendTelemetryEvent(eventName, telemetryEvent.Measurements, formattedProperties); - }); + const api = extension?.exports; + if (api && api.client && api.client.isEnabled()) { + this.pylanceApi = api; + await api.client.start(); + return; + } + + this.cancellationStrategy = new FileBasedCancellationStrategy(); + options.connectionOptions = { cancellationStrategy: this.cancellationStrategy }; + + const client = await this.factory.createLanguageClient(resource, interpreter, options); + this.registerHandlers(client, resource); + + this.disposables.push( + this.workspace.onDidGrantWorkspaceTrust(() => { + client.sendNotification('python/workspaceTrusted', { isTrusted: true }); + }), + ); + + await client.start(); + + this.languageClient = client; + } + + @traceDecoratorVerbose('Disposing language server') + public async stop(): Promise { + if (this.pylanceApi) { + const api = this.pylanceApi; + this.pylanceApi = undefined; + await api.client!.stop(); + } + + while (this.disposables.length > 0) { + const d = this.disposables.shift()!; + d.dispose(); + } + + if (this.languageClient) { + const client = this.languageClient; + this.languageClient = undefined; + + try { + await client.stop(); + await client.dispose(); + } catch (ex) { + traceError('Stopping language client failed', ex); } - await this.registerTestServices(); - } else { - await this.startupCompleted.promise; + } + + if (this.cancellationStrategy) { + this.cancellationStrategy.dispose(); + this.cancellationStrategy = undefined; } } - // tslint:disable-next-line: no-empty - public loadExtension(_args?: {}) {} + // eslint-disable-next-line class-methods-use-this + public loadExtension(): void { + // No body. + } @captureTelemetry( EventName.LANGUAGE_SERVER_READY, undefined, true, undefined, - NodeLanguageServerProxy.versionTelemetryProps + NodeLanguageServerProxy.versionTelemetryProps, ) - protected async serverReady(): Promise { - while (this.languageClient && !this.languageClient.initializeResult) { - await sleep(100); - } - if (this.languageClient) { - await this.languageClient.onReady(); - } - this.startupCompleted.resolve(); + private registerHandlers(client: LanguageClient, _resource: Resource) { + const progressReporting = new ProgressReporting(client); + this.disposables.push(progressReporting); + + this.disposables.push( + this.interpreterPathService.onDidChange(() => { + // Manually send didChangeConfiguration in order to get the server to requery + // the workspace configurations (to then pick up pythonPath set in the middleware). + // This is needed as interpreter changes via the interpreter path service happen + // outside of VS Code's settings (which would mean VS Code sends the config updates itself). + client.sendNotification(DidChangeConfigurationNotification.type, { + settings: null, + }); + }), + ); + this.disposables.push( + this.environmentService.onDidEnvironmentVariablesChange(() => { + client.sendNotification(DidChangeConfigurationNotification.type, { + settings: null, + }); + }), + ); + + client.onTelemetry((telemetryEvent) => { + const eventName = telemetryEvent.EventName || EventName.LANGUAGE_SERVER_TELEMETRY; + const formattedProperties = { + ...telemetryEvent.Properties, + // Replace all slashes in the method name so it doesn't get scrubbed by @vscode/extension-telemetry. + method: telemetryEvent.Properties.method?.replace(/\//g, '.'), + }; + sendTelemetryEvent(eventName, telemetryEvent.Measurements, formattedProperties, telemetryEvent.Exception); + }); + + client.onRequest( + InExperiment.Method, + async (params: InExperiment.IRequest): Promise => { + const inExperiment = await this.experimentService.inExperiment(params.experimentName); + return { inExperiment }; + }, + ); + + client.onRequest( + GetExperimentValue.Method, + async ( + params: GetExperimentValue.IRequest, + ): Promise> => { + const value = await this.experimentService.getExperimentValue(params.experimentName); + return { value }; + }, + ); + + this.disposables.push( + client.onRequest('python/isTrustedWorkspace', async () => ({ + isTrusted: this.workspace.isTrusted, + })), + ); } - @swallowExceptions('Activating Unit Tests Manager for Pylance language server') - protected async registerTestServices() { - if (!this.languageClient) { - throw new Error('languageClient not initialized'); + private async getPylanceExtension(): Promise | undefined> { + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (!extension) { + return undefined; + } + + if (!extension.isActive) { + await extension.activate(); } - await this.testManager.activate(new LanguageServerSymbolProvider(this.languageClient!)); + + return extension; } } diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index 8e21f079d29d..5a66e4abecd0 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -2,108 +2,107 @@ // Licensed under the MIT License. import '../../common/extensions'; -import { inject, injectable, named } from 'inversify'; - import { ICommandManager } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; -import { IConfigurationService, IDisposable, IExperimentsManager, Resource } from '../../common/types'; +import { IDisposable, IExtensions, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IServiceContainer } from '../../ioc/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry } from '../../telemetry'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { Commands } from '../commands'; -import { LanguageClientMiddleware } from '../languageClientMiddleware'; -import { - ILanguageServerAnalysisOptions, - ILanguageServerFolderService, - ILanguageServerManager, - ILanguageServerProxy, - LanguageServerType -} from '../types'; - -@injectable() +import { NodeLanguageClientMiddleware } from './languageClientMiddleware'; +import { ILanguageServerAnalysisOptions, ILanguageServerManager } from '../types'; +import { traceDecoratorError, traceDecoratorVerbose } from '../../logging'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { NodeLanguageServerProxy } from './languageServerProxy'; + export class NodeLanguageServerManager implements ILanguageServerManager { - private languageServerProxy?: ILanguageServerProxy; private resource!: Resource; - private interpreter: PythonInterpreter | undefined; - private middleware: LanguageClientMiddleware | undefined; + + private interpreter: PythonEnvironment | undefined; + + private middleware: NodeLanguageClientMiddleware | undefined; + private disposables: IDisposable[] = []; - private connected: boolean = false; + + private connected = false; + private lsVersion: string | undefined; + private started = false; + + private static commandDispose: IDisposable; + constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ILanguageServerAnalysisOptions) - @named(LanguageServerType.Node) + private readonly serviceContainer: IServiceContainer, private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ILanguageServerFolderService) - private readonly folderService: ILanguageServerFolderService, - @inject(IExperimentsManager) private readonly experimentsManager: IExperimentsManager, - @inject(IConfigurationService) private readonly configService: IConfigurationService, - @inject(ICommandManager) commandManager: ICommandManager + private readonly languageServerProxy: NodeLanguageServerProxy, + commandManager: ICommandManager, + private readonly extensions: IExtensions, ) { - this.disposables.push( - commandManager.registerCommand(Commands.RestartLS, () => { - this.restartLanguageServer().ignoreErrors(); - }) - ); + if (NodeLanguageServerManager.commandDispose) { + NodeLanguageServerManager.commandDispose.dispose(); + } + NodeLanguageServerManager.commandDispose = commandManager.registerCommand(Commands.RestartLS, () => { + sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'command' }); + this.restartLanguageServer().ignoreErrors(); + }); } private static versionTelemetryProps(instance: NodeLanguageServerManager) { return { - lsVersion: instance.lsVersion + lsVersion: instance.lsVersion, }; } - public dispose() { - if (this.languageProxy) { - this.languageProxy.dispose(); - } + public dispose(): void { + this.stopLanguageServer().ignoreErrors(); + NodeLanguageServerManager.commandDispose.dispose(); this.disposables.forEach((d) => d.dispose()); } - public get languageProxy() { - return this.languageServerProxy; - } - - @traceDecorators.error('Failed to start language server') - public async start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise { - if (this.languageProxy) { + @traceDecoratorError('Failed to start language server') + public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { + if (this.started) { throw new Error('Language server already started'); } this.resource = resource; this.interpreter = interpreter; this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); - const versionPair = await this.folderService.getCurrentLanguageServerDirectory(); - this.lsVersion = versionPair?.version.format(); + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + this.lsVersion = extension?.packageJSON.version || '0'; await this.analysisOptions.initialize(resource, interpreter); await this.startLanguageServer(); + + this.started = true; } - public connect() { - this.connected = true; - this.middleware?.connect(); + public connect(): void { + if (!this.connected) { + this.connected = true; + this.middleware?.connect(); + } } - public disconnect() { - this.connected = false; - this.middleware?.disconnect(); + public disconnect(): void { + if (this.connected) { + this.connected = false; + this.middleware?.disconnect(); + } } @debounceSync(1000) protected restartLanguageServerDebounced(): void { + sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'settings' }); this.restartLanguageServer().ignoreErrors(); } - @traceDecorators.error('Failed to restart language server') - @traceDecorators.verbose('Restarting language server') + @traceDecoratorError('Failed to restart language server') + @traceDecoratorVerbose('Restarting language server') protected async restartLanguageServer(): Promise { - if (this.languageProxy) { - this.languageProxy.dispose(); - } + await this.stopLanguageServer(); await this.startLanguageServer(); } @@ -112,19 +111,13 @@ export class NodeLanguageServerManager implements ILanguageServerManager { undefined, true, undefined, - NodeLanguageServerManager.versionTelemetryProps + NodeLanguageServerManager.versionTelemetryProps, ) - @traceDecorators.verbose('Starting language server') + @traceDecoratorVerbose('Starting language server') protected async startLanguageServer(): Promise { - this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); - - const options = await this.analysisOptions!.getAnalysisOptions(); - options.middleware = this.middleware = new LanguageClientMiddleware( - this.experimentsManager, - this.configService, - LanguageServerType.Node, - this.lsVersion - ); + const options = await this.analysisOptions.getAnalysisOptions(); + this.middleware = new NodeLanguageClientMiddleware(this.serviceContainer, this.lsVersion); + options.middleware = this.middleware; // Make sure the middleware is connected if we restart and we we're already connected. if (this.connected) { @@ -134,4 +127,11 @@ export class NodeLanguageServerManager implements ILanguageServerManager { // Then use this middleware to start a new language client. await this.languageServerProxy.start(this.resource, this.interpreter, options); } + + @traceDecoratorVerbose('Stopping language server') + protected async stopLanguageServer(): Promise { + if (this.languageServerProxy) { + await this.languageServerProxy.stop(); + } + } } diff --git a/src/client/activation/node/pylanceApi.ts b/src/client/activation/node/pylanceApi.ts new file mode 100644 index 000000000000..4b3d21d7527e --- /dev/null +++ b/src/client/activation/node/pylanceApi.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { + CancellationToken, + CompletionContext, + CompletionItem, + CompletionList, + Position, + TextDocument, + Uri, +} from 'vscode'; + +export interface PylanceApi { + client?: { + isEnabled(): boolean; + start(): Promise; + stop(): Promise; + }; + notebook?: { + registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; + getCompletionItems( + document: TextDocument, + position: Position, + context: CompletionContext, + token: CancellationToken, + ): Promise; + }; +} diff --git a/src/client/activation/none/activator.ts b/src/client/activation/none/activator.ts deleted file mode 100644 index 5266e8ab39c1..000000000000 --- a/src/client/activation/none/activator.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - WorkspaceEdit -} from 'vscode'; -import { isTestExecution } from '../../common/constants'; -import { BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner, Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { ILanguageServerActivator } from '../types'; - -/** - * Provides 'no language server' pseudo-activator. - * - * @export - * @class NoLanguageServerExtensionActivator - * @implements {ILanguageServerActivator} - */ -@injectable() -export class NoLanguageServerExtensionActivator implements ILanguageServerActivator { - constructor( - @inject(IPythonExtensionBanner) - @named(BANNER_NAME_PROPOSE_LS) - private proposePylancePopup: IPythonExtensionBanner - ) {} - public async start(_resource: Resource, _interpreter?: PythonInterpreter): Promise { - if (!isTestExecution()) { - this.proposePylancePopup.showBanner().ignoreErrors(); - } - } - // tslint:disable-next-line: no-empty - public dispose(): void {} - // tslint:disable-next-line: no-empty - public activate(): void {} - // tslint:disable-next-line: no-empty - public deactivate(): void {} - - public provideRenameEdits( - _document: TextDocument, - _position: Position, - _newName: string, - _token: CancellationToken - ): ProviderResult { - return null; - } - public provideDefinition( - _document: TextDocument, - _position: Position, - _token: CancellationToken - ): ProviderResult { - return null; - } - public provideHover( - _document: TextDocument, - _position: Position, - _token: CancellationToken - ): ProviderResult { - return null; - } - public provideReferences( - _document: TextDocument, - _position: Position, - _context: ReferenceContext, - _token: CancellationToken - ): ProviderResult { - return null; - } - public provideCompletionItems( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - _context: CompletionContext - ): ProviderResult { - return null; - } - public provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult { - return null; - } - public provideDocumentSymbols( - _document: TextDocument, - _token: CancellationToken - ): ProviderResult { - return null; - } - public provideSignatureHelp( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - _context: SignatureHelpContext - ): ProviderResult { - return null; - } -} diff --git a/src/client/activation/partialModeStatus.ts b/src/client/activation/partialModeStatus.ts new file mode 100644 index 000000000000..1105f6529ac8 --- /dev/null +++ b/src/client/activation/partialModeStatus.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import { inject, injectable } from 'inversify'; +import type * as vscodeTypes from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { IDisposableRegistry } from '../common/types'; +import { Common, LanguageService } from '../common/utils/localize'; +import { IExtensionSingleActivationService } from './types'; + +/** + * Only partial features are available when running in untrusted or a + * virtual workspace, this creates a UI element to indicate that. + */ +@injectable() +export class PartialModeStatusItem implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + constructor( + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + const { isTrusted, isVirtualWorkspace } = this.workspace; + if (isTrusted && !isVirtualWorkspace) { + return; + } + const statusItem = this.createStatusItem(); + if (statusItem) { + this.disposables.push(statusItem); + } + } + + private createStatusItem() { + // eslint-disable-next-line global-require + const vscode = require('vscode') as typeof vscodeTypes; + if ('createLanguageStatusItem' in vscode.languages) { + const statusItem = vscode.languages.createLanguageStatusItem('python.projectStatus', { + language: 'python', + }); + statusItem.name = LanguageService.statusItem.name; + statusItem.severity = vscode.LanguageStatusSeverity.Warning; + statusItem.text = LanguageService.statusItem.text; + statusItem.detail = !this.workspace.isTrusted + ? LanguageService.statusItem.detail + : LanguageService.virtualWorkspaceStatusItem.detail; + statusItem.command = { + title: Common.learnMore, + command: 'vscode.open', + arguments: [vscode.Uri.parse('https://aka.ms/AAdzyh4')], + }; + return statusItem; + } + return undefined; + } +} diff --git a/src/client/activation/progress.ts b/src/client/activation/progress.ts index 835a10188f59..5abcb9e553c0 100644 --- a/src/client/activation/progress.ts +++ b/src/client/activation/progress.ts @@ -31,7 +31,7 @@ export class ProgressReporting implements Disposable { if (!this.progress) { this.beginProgress(); } - this.progress!.report({ message: m }); + this.progress!.report({ message: m }); // NOSONAR }); this.languageClient.onNotification('python/endProgress', (_) => { @@ -55,12 +55,12 @@ export class ProgressReporting implements Disposable { window.withProgress( { location: ProgressLocation.Window, - title: '' + title: '', }, (progress) => { this.progress = progress; return this.progressDeferred!.promise; - } + }, ); } } diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts deleted file mode 100644 index e2bc6c833af0..000000000000 --- a/src/client/activation/refCountedLanguageServer.ts +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - TextDocumentContentChangeEvent, - WorkspaceEdit -} from 'vscode'; - -import { Resource } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { PythonInterpreter } from '../pythonEnvironments/info'; -import { ILanguageServerActivator, LanguageServerType } from './types'; - -export class RefCountedLanguageServer implements ILanguageServerActivator { - private refCount = 1; - constructor( - private impl: ILanguageServerActivator, - private _type: LanguageServerType, - private disposeCallback: () => void - ) {} - - public increment = () => { - this.refCount += 1; - }; - - public get type() { - return this._type; - } - - public dispose() { - this.refCount = Math.max(0, this.refCount - 1); - if (this.refCount === 0) { - this.disposeCallback(); - } - } - - public start(_resource: Resource, _interpreter: PythonInterpreter | undefined): Promise { - throw new Error('Server should have already been started. Do not start the wrapper.'); - } - - public activate() { - this.impl.activate(); - } - - public deactivate() { - this.impl.deactivate(); - } - - public clearAnalysisCache() { - this.impl.clearAnalysisCache ? this.impl.clearAnalysisCache() : noop(); - } - - public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { - this.impl.handleChanges ? this.impl.handleChanges(document, changes) : noop(); - } - - public handleOpen(document: TextDocument) { - this.impl.handleOpen ? this.impl.handleOpen(document) : noop(); - } - - public provideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken - ): ProviderResult { - return this.impl.provideRenameEdits(document, position, newName, token); - } - public provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken - ): ProviderResult { - return this.impl.provideDefinition(document, position, token); - } - public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - return this.impl.provideHover(document, position, token); - } - public provideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken - ): ProviderResult { - return this.impl.provideReferences(document, position, context, token); - } - public provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext - ): ProviderResult { - return this.impl.provideCompletionItems(document, position, token, context); - } - public resolveCompletionItem(item: CompletionItem, token: CancellationToken): ProviderResult { - if (this.impl.resolveCompletionItem) { - return this.impl.resolveCompletionItem(item, token); - } - } - public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { - return this.impl.provideCodeLenses(document, token); - } - public provideDocumentSymbols( - document: TextDocument, - token: CancellationToken - ): ProviderResult { - return this.impl.provideDocumentSymbols(document, token); - } - public provideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - context: SignatureHelpContext - ): ProviderResult { - return this.impl.provideSignatureHelp(document, position, token, context); - } -} diff --git a/src/client/activation/requirementsTxtLinkActivator.ts b/src/client/activation/requirementsTxtLinkActivator.ts new file mode 100644 index 000000000000..fcb6b72e545e --- /dev/null +++ b/src/client/activation/requirementsTxtLinkActivator.ts @@ -0,0 +1,26 @@ +import { injectable } from 'inversify'; +import { Hover, languages, TextDocument, Position } from 'vscode'; +import { IExtensionSingleActivationService } from './types'; + +const PYPI_PROJECT_URL = 'https://pypi.org/project'; + +export function generatePyPiLink(name: string): string | null { + // Regex to allow to find every possible pypi package (base regex from https://peps.python.org/pep-0508/#names) + const projectName = name.match(/^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*)($|=| |;|\[)/i); + return projectName ? `${PYPI_PROJECT_URL}/${projectName[1]}/` : null; +} + +@injectable() +export class RequirementsTxtLinkActivator implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + // eslint-disable-next-line class-methods-use-this + public async activate(): Promise { + languages.registerHoverProvider([{ pattern: '**/*requirement*.txt' }, { pattern: '**/requirements/*.txt' }], { + provideHover(document: TextDocument, position: Position) { + const link = generatePyPiLink(document.lineAt(position.line).text); + return link ? new Hover(link) : null; + }, + }); + } +} diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 0f1fa5497f67..875afa12f0b4 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -1,217 +1,43 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { registerTypes as registerDotNetTypes } from '../common/dotnet/serviceRegistry'; -import { INugetRepository } from '../common/nuget/types'; -import { - BANNER_NAME_DS_SURVEY, - BANNER_NAME_INTERACTIVE_SHIFTENTER, - BANNER_NAME_PROPOSE_LS, - IPythonExtensionBanner -} from '../common/types'; -import { DataScienceSurveyBanner } from '../datascience/dataScienceSurveyBanner'; -import { InteractiveShiftEnterBanner } from '../datascience/shiftEnterBanner'; import { IServiceManager } from '../ioc/types'; -import { ProposePylanceBanner } from '../languageServices/proposeLanguageServerBanner'; -import { AATesting } from './aaTesting'; import { ExtensionActivationManager } from './activationManager'; -import { LanguageServerExtensionActivationService } from './activationService'; -import { DownloadBetaChannelRule, DownloadDailyChannelRule } from './common/downloadChannelRules'; -import { LanguageServerDownloader } from './common/downloader'; -import { LanguageServerDownloadChannel } from './common/packageRepository'; import { ExtensionSurveyPrompt } from './extensionSurvey'; -import { JediExtensionActivator } from './jedi'; -import { DotNetLanguageServerActivator } from './languageServer/activator'; -import { DotNetLanguageServerAnalysisOptions } from './languageServer/analysisOptions'; -import { DotNetLanguageClientFactory } from './languageServer/languageClientFactory'; -import { LanguageServerCompatibilityService } from './languageServer/languageServerCompatibilityService'; -import { LanguageServerExtension } from './languageServer/languageServerExtension'; -import { DotNetLanguageServerFolderService } from './languageServer/languageServerFolderService'; -import { - BetaDotNetLanguageServerPackageRepository, - DailyDotNetLanguageServerPackageRepository, - StableDotNetLanguageServerPackageRepository -} from './languageServer/languageServerPackageRepository'; -import { DotNetLanguageServerPackageService } from './languageServer/languageServerPackageService'; -import { DotNetLanguageServerProxy } from './languageServer/languageServerProxy'; -import { DotNetLanguageServerManager } from './languageServer/manager'; -import { LanguageServerOutputChannel } from './languageServer/outputChannel'; -import { PlatformData } from './languageServer/platformData'; -import { NodeLanguageServerActivator } from './node/activator'; -import { NodeLanguageServerAnalysisOptions } from './node/analysisOptions'; -import { NodeLanguageClientFactory } from './node/languageClientFactory'; -import { NodeLanguageServerFolderService } from './node/languageServerFolderService'; -import { - BetaNodeLanguageServerPackageRepository, - DailyNodeLanguageServerPackageRepository, - StableNodeLanguageServerPackageRepository -} from './node/languageServerPackageRepository'; -import { NodeLanguageServerPackageService } from './node/languageServerPackageService'; -import { NodeLanguageServerProxy } from './node/languageServerProxy'; -import { NodeLanguageServerManager } from './node/manager'; -import { NoLanguageServerExtensionActivator } from './none/activator'; +import { LanguageServerOutputChannel } from './common/outputChannel'; import { - IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, - ILanguageClientFactory, - ILanguageServerActivator, - ILanguageServerAnalysisOptions, - ILanguageServerCache, - ILanguageServerCompatibilityService, - ILanguageServerDownloader, - ILanguageServerExtension, - ILanguageServerFolderService, - ILanguageServerManager, ILanguageServerOutputChannel, - ILanguageServerPackageService, - ILanguageServerProxy, - IPlatformData, - LanguageServerType } from './types'; +import { LoadLanguageServerExtension } from './common/loadLanguageServerExtension'; +import { PartialModeStatusItem } from './partialModeStatus'; +import { ILanguageServerWatcher } from '../languageServer/types'; +import { LanguageServerWatcher } from '../languageServer/watcher'; +import { RequirementsTxtLinkActivator } from './requirementsTxtLinkActivator'; -// tslint:disable-next-line: max-func-body-length -export function registerTypes(serviceManager: IServiceManager, languageServerType: LanguageServerType) { - serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); - serviceManager.addBinding(ILanguageServerCache, IExtensionActivationService); - serviceManager.addSingleton(ILanguageServerExtension, LanguageServerExtension); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton(IExtensionActivationService, PartialModeStatusItem); serviceManager.add(IExtensionActivationManager, ExtensionActivationManager); - - serviceManager.add( - ILanguageServerActivator, - JediExtensionActivator, - LanguageServerType.Jedi - ); - - serviceManager.addSingleton( - IPythonExtensionBanner, - ProposePylanceBanner, - BANNER_NAME_PROPOSE_LS + serviceManager.addSingleton( + ILanguageServerOutputChannel, + LanguageServerOutputChannel, ); - serviceManager.addSingleton( - IPythonExtensionBanner, - DataScienceSurveyBanner, - BANNER_NAME_DS_SURVEY + serviceManager.addSingleton( + IExtensionSingleActivationService, + ExtensionSurveyPrompt, ); - serviceManager.addSingleton( - IPythonExtensionBanner, - InteractiveShiftEnterBanner, - BANNER_NAME_INTERACTIVE_SHIFTENTER + serviceManager.addSingleton( + IExtensionSingleActivationService, + LoadLanguageServerExtension, ); - if (languageServerType === LanguageServerType.Microsoft) { - serviceManager.add( - ILanguageServerAnalysisOptions, - DotNetLanguageServerAnalysisOptions, - LanguageServerType.Microsoft - ); - serviceManager.add( - ILanguageServerActivator, - DotNetLanguageServerActivator, - LanguageServerType.Microsoft - ); - serviceManager.addSingleton( - INugetRepository, - StableDotNetLanguageServerPackageRepository, - LanguageServerDownloadChannel.stable - ); - serviceManager.addSingleton( - INugetRepository, - BetaDotNetLanguageServerPackageRepository, - LanguageServerDownloadChannel.beta - ); - serviceManager.addSingleton( - INugetRepository, - DailyDotNetLanguageServerPackageRepository, - LanguageServerDownloadChannel.daily - ); - serviceManager.addSingleton( - ILanguageServerCompatibilityService, - LanguageServerCompatibilityService - ); - serviceManager.addSingleton(ILanguageClientFactory, DotNetLanguageClientFactory); - serviceManager.addSingleton(IPlatformData, PlatformData); - serviceManager.add(ILanguageServerManager, DotNetLanguageServerManager); - serviceManager.add(ILanguageServerProxy, DotNetLanguageServerProxy); - serviceManager.addSingleton( - ILanguageServerFolderService, - DotNetLanguageServerFolderService - ); - serviceManager.addSingleton( - ILanguageServerPackageService, - DotNetLanguageServerPackageService - ); - registerDotNetTypes(serviceManager); - } else if (languageServerType === LanguageServerType.Node) { - serviceManager.add( - ILanguageServerAnalysisOptions, - NodeLanguageServerAnalysisOptions, - LanguageServerType.Node - ); - serviceManager.add( - ILanguageServerActivator, - NodeLanguageServerActivator, - LanguageServerType.Node - ); - serviceManager.addSingleton( - INugetRepository, - StableNodeLanguageServerPackageRepository, - LanguageServerDownloadChannel.stable - ); - serviceManager.addSingleton( - INugetRepository, - BetaNodeLanguageServerPackageRepository, - LanguageServerDownloadChannel.beta - ); - serviceManager.addSingleton( - INugetRepository, - DailyNodeLanguageServerPackageRepository, - LanguageServerDownloadChannel.daily - ); - serviceManager.addSingleton(ILanguageClientFactory, NodeLanguageClientFactory); - serviceManager.add(ILanguageServerManager, NodeLanguageServerManager); - serviceManager.add(ILanguageServerProxy, NodeLanguageServerProxy); - serviceManager.addSingleton( - ILanguageServerFolderService, - NodeLanguageServerFolderService - ); - serviceManager.addSingleton( - ILanguageServerPackageService, - NodeLanguageServerPackageService - ); - } else if (languageServerType === LanguageServerType.None) { - serviceManager.add( - ILanguageServerActivator, - NoLanguageServerExtensionActivator, - LanguageServerType.None - ); - } + serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); + serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); - serviceManager.addSingleton( - IDownloadChannelRule, - DownloadDailyChannelRule, - LanguageServerDownloadChannel.daily - ); - serviceManager.addSingleton( - IDownloadChannelRule, - DownloadBetaChannelRule, - LanguageServerDownloadChannel.beta - ); - serviceManager.addSingleton( - IDownloadChannelRule, - DownloadBetaChannelRule, - LanguageServerDownloadChannel.stable - ); - serviceManager.addSingleton(ILanguageServerDownloader, LanguageServerDownloader); - - serviceManager.addSingleton( - ILanguageServerOutputChannel, - LanguageServerOutputChannel - ); serviceManager.addSingleton( IExtensionSingleActivationService, - ExtensionSurveyPrompt + RequirementsTxtLinkActivator, ); - serviceManager.addSingleton(IExtensionSingleActivationService, AATesting); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index b51ad0c42fbe..e3b9b818691a 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -3,49 +3,23 @@ 'use strict'; -import { SemVer } from 'semver'; -import { - CodeLensProvider, - CompletionItemProvider, - DefinitionProvider, - DocumentSymbolProvider, - Event, - HoverProvider, - ReferenceProvider, - RenameProvider, - SignatureHelpProvider, - TextDocument, - TextDocumentContentChangeEvent -} from 'vscode'; +import { Event } from 'vscode'; import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; -import { NugetPackage } from '../common/nuget/types'; -import { IDisposable, IOutputChannel, LanguageServerDownloadChannels, Resource } from '../common/types'; -import { PythonInterpreter } from '../pythonEnvironments/info'; +import type { IDisposable, ILogOutputChannel, Resource } from '../common/types'; +import { StopWatch } from '../common/utils/stopWatch'; +import { PythonEnvironment } from '../pythonEnvironments/info'; export const IExtensionActivationManager = Symbol('IExtensionActivationManager'); /** * Responsible for activation of extension. - * - * @export - * @interface IExtensionActivationManager - * @extends {IDisposable} */ export interface IExtensionActivationManager extends IDisposable { - /** - * Method invoked when extension activates (invoked once). - * - * @returns {Promise} - * @memberof IExtensionActivationManager - */ - activate(): Promise; + // Method invoked when extension activates (invoked once). + activate(startupStopWatch: StopWatch): Promise; /** * Method invoked when a workspace is loaded. * This is where we place initialization scripts for each workspace. * (e.g. if we need to run code for each workspace, then this is where that happens). - * - * @param {Resource} resource - * @returns {Promise} - * @memberof IExtensionActivationManager */ activateWorkspace(resource: Resource): Promise; } @@ -56,167 +30,71 @@ export const IExtensionActivationService = Symbol('IExtensionActivationService') * invoked for every workspace folder (in multi-root workspace folders) during the activation of the extension. * This is a great hook for extension activation code, i.e. you don't need to modify * the `extension.ts` file to invoke some code when extension gets activated. - * @export - * @interface IExtensionActivationService */ export interface IExtensionActivationService { - activate(resource: Resource): Promise; + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean }; + activate(resource: Resource, startupStopWatch?: StopWatch): Promise; } export enum LanguageServerType { Jedi = 'Jedi', + JediLSP = 'JediLSP', Microsoft = 'Microsoft', Node = 'Pylance', - None = 'None' -} - -export const DotNetLanguageServerFolder = 'languageServer'; -export const NodeLanguageServerFolder = 'nodeLanguageServer'; - -// tslint:disable-next-line: interface-name -export interface DocumentHandler { - handleOpen(document: TextDocument): void; - handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void; -} - -// tslint:disable-next-line: interface-name -export interface LanguageServerCommandHandler { - clearAnalysisCache(): void; + None = 'None', } -export interface ILanguageServer - extends RenameProvider, - DefinitionProvider, - HoverProvider, - ReferenceProvider, - CompletionItemProvider, - CodeLensProvider, - DocumentSymbolProvider, - SignatureHelpProvider, - Partial, - Partial, - IDisposable {} - export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); -export interface ILanguageServerActivator extends ILanguageServer { - start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; +export interface ILanguageServerActivator { + start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise; activate(): void; deactivate(): void; } -export const ILanguageServerCache = Symbol('ILanguageServerCache'); -export interface ILanguageServerCache { - get(resource: Resource, interpreter?: PythonInterpreter): Promise; -} - -export type FolderVersionPair = { path: string; version: SemVer }; -export const ILanguageServerFolderService = Symbol('ILanguageServerFolderService'); - -export interface ILanguageServerFolderService { - getLanguageServerFolderName(resource: Resource): Promise; - getLatestLanguageServerVersion(resource: Resource): Promise; - getCurrentLanguageServerDirectory(): Promise; - skipDownload(): Promise; -} - -export const ILanguageServerDownloader = Symbol('ILanguageServerDownloader'); - -export interface ILanguageServerDownloader { - downloadLanguageServer(destinationFolder: string, resource: Resource): Promise; -} - -export const ILanguageServerPackageService = Symbol('ILanguageServerPackageService'); -export interface ILanguageServerPackageService { - getNugetPackageName(): string; - getLatestNugetPackageVersion(resource: Resource, minVersion?: string): Promise; - getLanguageServerDownloadChannel(): LanguageServerDownloadChannels; -} - -export const MajorLanguageServerVersion = Symbol('MajorLanguageServerVersion'); -export const IDownloadChannelRule = Symbol('IDownloadChannelRule'); -export interface IDownloadChannelRule { - shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise; -} -export const ILanguageServerCompatibilityService = Symbol('ILanguageServerCompatibilityService'); -export interface ILanguageServerCompatibilityService { - isSupported(): Promise; -} -export enum LanguageClientFactory { - base = 'base', - simple = 'simple', - downloaded = 'downloaded' -} export const ILanguageClientFactory = Symbol('ILanguageClientFactory'); export interface ILanguageClientFactory { createLanguageClient( resource: Resource, - interpreter: PythonInterpreter | undefined, + interpreter: PythonEnvironment | undefined, clientOptions: LanguageClientOptions, - env?: NodeJS.ProcessEnv + env?: NodeJS.ProcessEnv, ): Promise; } export const ILanguageServerAnalysisOptions = Symbol('ILanguageServerAnalysisOptions'); export interface ILanguageServerAnalysisOptions extends IDisposable { readonly onDidChange: Event; - initialize(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; + initialize(resource: Resource, interpreter: PythonEnvironment | undefined): Promise; getAnalysisOptions(): Promise; } export const ILanguageServerManager = Symbol('ILanguageServerManager'); export interface ILanguageServerManager extends IDisposable { - readonly languageProxy: ILanguageServerProxy | undefined; - start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; + start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise; connect(): void; disconnect(): void; } -export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); -export interface ILanguageServerExtension extends IDisposable { - readonly invoked: Event; - loadExtensionArgs?: {}; - register(): void; -} + export const ILanguageServerProxy = Symbol('ILanguageServerProxy'); export interface ILanguageServerProxy extends IDisposable { - /** - * LanguageClient in use - */ - languageClient: LanguageClient | undefined; start( resource: Resource, - interpreter: PythonInterpreter | undefined, - options: LanguageClientOptions + interpreter: PythonEnvironment | undefined, + options: LanguageClientOptions, ): Promise; + stop(): Promise; /** * Sends a request to LS so as to load other extensions. * This is used as a plugin loader mechanism. * Anyone (such as intellicode) wanting to interact with LS, needs to send this request to LS. - * @param {{}} [args] - * @memberof ILanguageServerProxy */ - loadExtension(args?: {}): void; -} - -export enum PlatformName { - Windows32Bit = 'win-x86', - Windows64Bit = 'win-x64', - Mac64Bit = 'osx-x64', - Linux64Bit = 'linux-x64' -} -export const IPlatformData = Symbol('IPlatformData'); -export interface IPlatformData { - readonly platformName: PlatformName; - readonly engineDllName: string; - readonly engineExecutableName: string; + loadExtension(args?: unknown): void; } export const ILanguageServerOutputChannel = Symbol('ILanguageServerOutputChannel'); export interface ILanguageServerOutputChannel { /** * Creates output channel if necessary and returns it - * - * @type {IOutputChannel} - * @memberof ILanguageServerOutputChannel */ - readonly channel: IOutputChannel; + readonly channel: ILogOutputChannel; } export const IExtensionSingleActivationService = Symbol('IExtensionSingleActivationService'); @@ -225,9 +103,8 @@ export const IExtensionSingleActivationService = Symbol('IExtensionSingleActivat * invoked during the activation of the extension. * This is a great hook for extension activation code, i.e. you don't need to modify * the `extension.ts` file to invoke some code when extension gets activated. - * @export - * @interface IExtensionSingleActivationService */ export interface IExtensionSingleActivationService { + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean }; activate(): Promise; } diff --git a/src/client/api.ts b/src/client/api.ts index 389f2912eada..908da4be7103 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -1,154 +1,169 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { Event, Uri } from 'vscode'; -import { isTestExecution } from './common/constants'; -import { traceError } from './common/logger'; +import { Uri, Event } from 'vscode'; +import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; +import { LanguageClient } from 'vscode-languageclient/node'; +import { PYLANCE_NAME } from './activation/node/languageClientFactory'; +import { ILanguageServerOutputChannel } from './activation/types'; +import { PythonExtension } from './api/types'; +import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; -import { IDataViewerDataProvider, IDataViewerFactory } from './datascience/data-viewing/types'; -import { IJupyterUriProvider, IJupyterUriProviderRegistration } from './datascience/types'; -import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; +import { getDebugpyLauncherArgs } from './debugger/extension/adapter/remoteLaunchers'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; +import { + JupyterExtensionIntegration, + JupyterExtensionPythonEnvironments, + JupyterPythonEnvironmentApi, +} from './jupyter/jupyterIntegration'; +import { traceError } from './logging'; +import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { buildEnvironmentApi } from './environmentApi'; +import { ApiForPylance } from './pylanceApi'; +import { getTelemetryReporter } from './telemetry'; +import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; +import { getDebugpyPath } from './debugger/pythonDebugger'; -/* - * Do not introduce any breaking changes to this API. - * This is the public API for other extensions to interact with this extension. - */ +export function buildApi( + ready: Promise, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer, + discoveryApi: IDiscoveryAPI, +): PythonExtension { + const configurationService = serviceContainer.get(IConfigurationService); + const interpreterService = serviceContainer.get(IInterpreterService); + serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + JupyterExtensionPythonEnvironments, + JupyterExtensionPythonEnvironments, + ); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); + const jupyterPythonEnvApi = serviceContainer.get(JupyterExtensionPythonEnvironments); + const environments = buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi); + const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + jupyterIntegration.registerEnvApi(environments); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); + const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); -export interface IExtensionApi { - /** - * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise} - * @memberof IExtensionApi - */ - ready: Promise; - debug: { + const api: PythonExtension & { /** - * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. - * Users can append another array of strings of what they want to execute along with relevant arguments to Python. - * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise} + * Internal API just for Jupyter, hence don't include in the official types. */ - getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; - + jupyter: { + registerHooks(): void; + }; /** - * Gets the path to the debugger package used by the extension. - * @returns {Promise} + * Internal API just for Tensorboard, hence don't include in the official types. */ - getDebuggerPackagePath(): Promise; - }; - /** - * Return internal settings within the extension which are stored in VSCode storage - */ - settings: { + tensorboard: { + registerHooks(): void; + }; + } & { /** - * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. + * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an + * iteration or two. */ - readonly onDidChangeExecutionDetails: Event; + pylance: ApiForPylance; + } & { /** - * Returns all the details the consumer needs to execute code within the selected environment, - * corresponding to the specified resource taking into account any workspace-specific settings - * for the workspace to which this resource belongs. - * @param {Resource} [resource] A resource for which the setting is asked for. - * * When no resource is provided, the setting scoped to the first workspace folder is returned. - * * If no folder is present, it returns the global setting. - * @returns {({ execCommand: string[] | undefined })} + * @deprecated Use PythonExtension.environments API instead. + * + * Return internal settings within the extension which are stored in VSCode storage */ - getExecutionDetails( - resource?: Resource - ): { + settings: { + /** + * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. + */ + readonly onDidChangeExecutionDetails: Event; /** - * E.g of execution commands returned could be, - * * `['']` - * * `['']` - * * `['conda', 'run', 'python']` which is used to run from within Conda environments. - * or something similar for some other Python environments. - * - * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. - * Otherwise, join the items returned using space to construct the full execution command. + * Returns all the details the consumer needs to execute code within the selected environment, + * corresponding to the specified resource taking into account any workspace-specific settings + * for the workspace to which this resource belongs. + * @param {Resource} [resource] A resource for which the setting is asked for. + * * When no resource is provided, the setting scoped to the first workspace folder is returned. + * * If no folder is present, it returns the global setting. */ - execCommand: string[] | undefined; + getExecutionDetails( + resource?: Resource, + ): { + /** + * E.g of execution commands returned could be, + * * `['']` + * * `['']` + * * `['conda', 'run', 'python']` which is used to run from within Conda environments. + * or something similar for some other Python environments. + * + * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. + * Otherwise, join the items returned using space to construct the full execution command. + */ + execCommand: string[] | undefined; + }; }; - }; - 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; - /** - * 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 function buildApi( - // tslint:disable-next-line:no-any - ready: Promise, - serviceManager: IServiceManager, - serviceContainer: IServiceContainer -): IExtensionApi { - const configurationService = serviceContainer.get(IConfigurationService); - const interpreterService = serviceContainer.get(IInterpreterService); - const api: IExtensionApi = { + } = { // 'ready' will propagate the exception, but we must log it here first. ready: ready.catch((ex) => { traceError('Failure during activation.', ex); return Promise.reject(ex); }), + jupyter: { + registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), + }, + tensorboard: { + registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), + }, debug: { async getRemoteLauncherCommand( host: string, port: number, - waitUntilDebuggerAttaches: boolean = true + waitUntilDebuggerAttaches = true, ): Promise { return getDebugpyLauncherArgs({ host, port, - waitUntilDebuggerAttaches + waitUntilDebuggerAttaches, }); }, async getDebuggerPackagePath(): Promise { - return getDebugpyPackagePath(); - } + return getDebugpyPath(); + }, }, settings: { onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, getExecutionDetails(resource?: Resource) { - const pythonPath = configurationService.getSettings(resource).pythonPath; + const { pythonPath } = configurationService.getSettings(resource); // If pythonPath equals an empty string, no interpreter is set. return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; - } + }, }, - datascience: { - async showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise { - const dataViewerProviderService = serviceContainer.get(IDataViewerFactory); - await dataViewerProviderService.create(dataProvider, title); + pylance: { + createClient: (...args: any[]): BaseLanguageClient => { + // Make sure we share output channel so that we can share one with + // Jedi as well. + const clientOptions = args[1] as LanguageClientOptions; + clientOptions.outputChannel = clientOptions.outputChannel ?? outputChannel.channel; + + return new LanguageClient(PYTHON_LANGUAGE, PYLANCE_NAME, args[0], clientOptions); }, - registerRemoteServerProvider(picker: IJupyterUriProvider): void { - const container = serviceContainer.get( - IJupyterUriProviderRegistration - ); - container.registerProvider(picker); - } - } + start: (client: BaseLanguageClient): Promise => client.start(), + stop: (client: BaseLanguageClient): Promise => client.stop(), + getTelemetryReporter: () => getTelemetryReporter(), + }, + environments, }; // In test environment return the DI Container. if (isTestExecution()) { - // tslint:disable:no-any (api as any).serviceContainer = serviceContainer; (api as any).serviceManager = serviceManager; - // tslint:enable:no-any } return api; } diff --git a/src/client/api/types.ts b/src/client/api/types.ts new file mode 100644 index 000000000000..95556aacbd90 --- /dev/null +++ b/src/client/api/types.ts @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ +export interface PythonExtension { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + */ + ready: Promise; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/python_files/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + + /** + * Gets the path to the debugger package used by the extension. + */ + getDebuggerPackagePath(): Promise; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Resource the environment changed for. + */ + readonly resource: Resource | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Hatch' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts index aebfa942edb7..90d2ced8d0ae 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -1,72 +1,70 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - -import { inject, injectable, named } from 'inversify'; +import { inject, injectable } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; -import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import { traceError, traceInfo, traceWarning } from '../../common/logger'; -import { IOutputChannel, Resource } from '../../common/types'; +import { IWorkspaceService } from '../../common/application/types'; +import { isTestExecution } from '../../common/constants'; +import { Resource } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; +import { traceLog, traceVerbose } from '../../logging'; import { IApplicationDiagnostics } from '../types'; -import { IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from './types'; +import { IDiagnostic, IDiagnosticsService } from './types'; + +function log(diagnostics: IDiagnostic[]): void { + diagnostics.forEach((item) => { + const message = `Diagnostic Code: ${item.code}, Message: ${item.message}`; + switch (item.severity) { + case DiagnosticSeverity.Error: + case DiagnosticSeverity.Warning: { + traceLog(message); + break; + } + default: { + traceVerbose(message); + } + } + }); +} + +async function runDiagnostics(diagnosticServices: IDiagnosticsService[], resource: Resource): Promise { + await Promise.all( + diagnosticServices.map(async (diagnosticService) => { + const diagnostics = await diagnosticService.diagnose(resource); + if (diagnostics.length > 0) { + log(diagnostics); + await diagnosticService.handle(diagnostics); + } + }), + ); +} @injectable() export class ApplicationDiagnostics implements IApplicationDiagnostics { - constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel - ) {} - public register() { - this.serviceContainer.get(ISourceMapSupportService).register(); - } + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + + public register() {} + public async performPreStartupHealthCheck(resource: Resource): Promise { // When testing, do not perform health checks, as modal dialogs can be displayed. if (isTestExecution()) { return; } - const services = this.serviceContainer.getAll(IDiagnosticsService); + let services = this.serviceContainer.getAll(IDiagnosticsService); + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (!workspaceService.isTrusted) { + services = services.filter((item) => item.runInUntrustedWorkspace); + } // Perform these validation checks in the foreground. - await this.runDiagnostics( + await runDiagnostics( services.filter((item) => !item.runInBackground), - resource + resource, ); + // Perform these validation checks in the background. - this.runDiagnostics( + runDiagnostics( services.filter((item) => item.runInBackground), - resource + resource, ).ignoreErrors(); } - private async runDiagnostics(diagnosticServices: IDiagnosticsService[], resource: Resource): Promise { - await Promise.all( - diagnosticServices.map(async (diagnosticService) => { - const diagnostics = await diagnosticService.diagnose(resource); - if (diagnostics.length > 0) { - this.log(diagnostics); - await diagnosticService.handle(diagnostics); - } - }) - ); - } - private log(diagnostics: IDiagnostic[]): void { - diagnostics.forEach((item) => { - const message = `Diagnostic Code: ${item.code}, Message: ${item.message}`; - switch (item.severity) { - case DiagnosticSeverity.Error: { - traceError(message); - this.outputChannel.appendLine(message); - break; - } - case DiagnosticSeverity.Warning: { - traceWarning(message); - this.outputChannel.appendLine(message); - break; - } - default: { - traceInfo(message); - } - } - }); - } } diff --git a/src/client/application/diagnostics/base.ts b/src/client/application/diagnostics/base.ts index 240883cb3657..8ce1c3b83184 100644 --- a/src/client/application/diagnostics/base.ts +++ b/src/client/application/diagnostics/base.ts @@ -7,6 +7,7 @@ import { injectable, unmanaged } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; import { IDisposable, IDisposableRegistry, Resource } from '../../common/types'; +import { asyncFilter } from '../../common/utils/arrayUtils'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -21,8 +22,8 @@ export abstract class BaseDiagnostic implements IDiagnostic { public readonly severity: DiagnosticSeverity, public readonly scope: DiagnosticScope, public readonly resource: Resource, + public readonly shouldShowPrompt = true, public readonly invokeHandler: 'always' | 'default' = 'default', - public readonly shouldShowPrompt = true ) {} } @@ -33,8 +34,9 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi constructor( @unmanaged() private readonly supportedDiagnosticCodes: string[], @unmanaged() protected serviceContainer: IServiceContainer, - @unmanaged() disposableRegistry: IDisposableRegistry, - @unmanaged() public readonly runInBackground: Boolean = false + @unmanaged() protected disposableRegistry: IDisposableRegistry, + @unmanaged() public readonly runInBackground: boolean = false, + @unmanaged() public readonly runInUntrustedWorkspace: boolean = false, ) { this.filterService = serviceContainer.get(IDiagnosticFilterService); disposableRegistry.push(this); @@ -47,7 +49,10 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi if (diagnostics.length === 0) { return; } - const diagnosticsToHandle = diagnostics.filter((item) => { + const diagnosticsToHandle = await asyncFilter(diagnostics, async (item) => { + if (!(await this.canHandle(item))) { + return false; + } if (item.invokeHandler && item.invokeHandler === 'always') { return true; } @@ -68,11 +73,6 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi /** * Returns a key used to keep track of whether a diagnostic was handled or not. * So as to prevent handling/displaying messages multiple times for the same diagnostic. - * - * @protected - * @param {IDiagnostic} diagnostic - * @returns {string} - * @memberof BaseDiagnosticsService */ protected getDiagnosticsKey(diagnostic: IDiagnostic): string { if (diagnostic.scope === DiagnosticScope.Global) { diff --git a/src/client/application/diagnostics/checks/envPathVariable.ts b/src/client/application/diagnostics/checks/envPathVariable.ts index 2c137662055e..b8850b8bbeee 100644 --- a/src/client/application/diagnostics/checks/envPathVariable.ts +++ b/src/client/application/diagnostics/checks/envPathVariable.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; import { IApplicationEnvironment } from '../../../common/application/types'; import '../../../common/extensions'; import { IPlatformService } from '../../../common/platform/types'; import { ICurrentProcess, IDisposableRegistry, IPathUtils, Resource } from '../../../common/types'; +import { Common } from '../../../common/utils/localize'; import { IServiceContainer } from '../../../ioc/types'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; @@ -20,14 +20,14 @@ const InvalidEnvPathVariableMessage = "The environment variable '{0}' seems to have some paths containing the '\"' character." + " The existence of such a character is known to have caused the {1} extension to not load. If the extension fails to load please modify your paths to remove this '\"' character."; -export class InvalidEnvironmentPathVariableDiagnostic extends BaseDiagnostic { +class InvalidEnvironmentPathVariableDiagnostic extends BaseDiagnostic { constructor(message: string, resource: Resource) { super( DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic, message, DiagnosticSeverity.Warning, DiagnosticScope.Global, - resource + resource, ); } } @@ -37,27 +37,36 @@ export const EnvironmentPathVariableDiagnosticsServiceId = 'EnvironmentPathVaria @injectable() export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsService { protected readonly messageService: IDiagnosticHandlerService; + private readonly platform: IPlatformService; + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { - super([DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], serviceContainer, disposableRegistry, true); + super( + [DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], + serviceContainer, + disposableRegistry, + true, + true, + ); this.platform = this.serviceContainer.get(IPlatformService); this.messageService = serviceContainer.get>( IDiagnosticHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); } + public async diagnose(resource: Resource): Promise { if (this.platform.isWindows && this.doesPathVariableHaveInvalidEntries()) { const env = this.serviceContainer.get(IApplicationEnvironment); const message = InvalidEnvPathVariableMessage.format(this.platform.pathVariableName, env.extensionName); return [new InvalidEnvironmentPathVariableDiagnostic(message, resource)]; - } else { - return []; } + return []; } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { @@ -70,20 +79,21 @@ export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsSe const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); const options = [ { - prompt: 'Ignore' + prompt: Common.ignore, }, { - prompt: 'Always Ignore', - command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) + prompt: Common.alwaysIgnore, + command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }), }, { - prompt: 'More Info', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/Niq35h' }) - } + prompt: Common.moreInfo, + command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/Niq35h' }), + }, ]; await this.messageService.handle(diagnostic, { commandPrompts: options }); } + private doesPathVariableHaveInvalidEntries() { const currentProc = this.serviceContainer.get(ICurrentProcess); const pathValue = currentProc.env[this.platform.pathVariableName]; diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts index a545764d26ec..440ff16856d3 100644 --- a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; @@ -18,10 +17,10 @@ import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '. import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; const messages = { - [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: Diagnostics.invalidDebuggerTypeDiagnostic(), - [DiagnosticCodes.JustMyCodeDiagnostic]: Diagnostics.justMyCodeDiagnostic(), - [DiagnosticCodes.ConsoleTypeDiagnostic]: Diagnostics.consoleTypeDiagnostic(), - [DiagnosticCodes.ConfigPythonPathDiagnostic]: '' + [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: Diagnostics.invalidDebuggerTypeDiagnostic, + [DiagnosticCodes.JustMyCodeDiagnostic]: Diagnostics.justMyCodeDiagnostic, + [DiagnosticCodes.ConsoleTypeDiagnostic]: Diagnostics.consoleTypeDiagnostic, + [DiagnosticCodes.ConfigPythonPathDiagnostic]: '', }; export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { @@ -32,7 +31,7 @@ export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { | DiagnosticCodes.ConsoleTypeDiagnostic | DiagnosticCodes.ConfigPythonPathDiagnostic, resource: Resource, - shouldShowPrompt: boolean = true + shouldShowPrompt = true, ) { super( code, @@ -40,8 +39,7 @@ export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource, - 'always', - shouldShowPrompt + shouldShowPrompt, ); } } @@ -57,22 +55,24 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IDiagnosticHandlerService) @named(DiagnosticCommandPromptHandlerServiceId) - private readonly messageService: IDiagnosticHandlerService + private readonly messageService: IDiagnosticHandlerService, ) { super( [ DiagnosticCodes.InvalidDebuggerTypeDiagnostic, DiagnosticCodes.JustMyCodeDiagnostic, DiagnosticCodes.ConsoleTypeDiagnostic, - DiagnosticCodes.ConfigPythonPathDiagnostic + DiagnosticCodes.ConfigPythonPathDiagnostic, ], serviceContainer, disposableRegistry, - true + true, ); } + public async diagnose(resource: Resource): Promise { - if (!this.workspaceService.hasWorkspaceFolders) { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { return []; } const workspaceFolder = resource @@ -80,22 +80,26 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { : this.workspaceService.workspaceFolders![0]; return this.diagnoseWorkspace(workspaceFolder, resource); } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic)); } - protected async fixLaunchJson(code: DiagnosticCodes) { - if (!this.workspaceService.hasWorkspaceFolders) { + + protected async fixLaunchJson(code: DiagnosticCodes): Promise { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { return; } await Promise.all( - this.workspaceService.workspaceFolders!.map((workspaceFolder) => - this.fixLaunchJsonInWorkspace(code, workspaceFolder) - ) + (this.workspaceService.workspaceFolders ?? []).map((workspaceFolder) => + this.fixLaunchJsonInWorkspace(code, workspaceFolder), + ), ); } + private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { - const launchJson = this.getLaunchJsonFile(workspaceFolder); + const launchJson = getLaunchJsonFile(workspaceFolder); if (!(await this.fs.fileExists(launchJson))) { return []; } @@ -104,7 +108,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { const diagnostics: IDiagnostic[] = []; if (fileContents.indexOf('"pythonExperimental"') > 0) { diagnostics.push( - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource) + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource), ); } if (fileContents.indexOf('"debugStdLib"') > 0) { @@ -114,70 +118,72 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource)); } if ( + fileContents.indexOf('"pythonPath":') > 0 || fileContents.indexOf('{config:python.pythonPath}') > 0 || fileContents.indexOf('{config:python.interpreterPath}') > 0 ) { diagnostics.push( - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false) + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false), ); } return diagnostics; } + private async handleDiagnostic(diagnostic: IDiagnostic): Promise { - if (!this.canHandle(diagnostic)) { - return; - } if (!diagnostic.shouldShowPrompt) { - return this.fixLaunchJson(diagnostic.code); + await this.fixLaunchJson(diagnostic.code); + return; } const commandPrompts = [ { - prompt: Diagnostics.yesUpdateLaunch(), + prompt: Diagnostics.yesUpdateLaunch, command: { diagnostic, invoke: async (): Promise => { await this.fixLaunchJson(diagnostic.code); - } - } + }, + }, }, { - prompt: Common.noIWillDoItLater() - } + prompt: Common.noIWillDoItLater, + }, ]; await this.messageService.handle(diagnostic, { commandPrompts }); } + private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) { if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) { return; } - const launchJson = this.getLaunchJsonFile(workspaceFolder); + const launchJson = getLaunchJsonFile(workspaceFolder); let fileContents = await this.fs.readFile(launchJson); switch (code) { case DiagnosticCodes.InvalidDebuggerTypeDiagnostic: { - fileContents = this.findAndReplace(fileContents, '"pythonExperimental"', '"python"'); - fileContents = this.findAndReplace(fileContents, '"Python Experimental:', '"Python:'); + fileContents = findAndReplace(fileContents, '"pythonExperimental"', '"python"'); + fileContents = findAndReplace(fileContents, '"Python Experimental:', '"Python:'); break; } case DiagnosticCodes.JustMyCodeDiagnostic: { - fileContents = this.findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); - fileContents = this.findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); + fileContents = findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); + fileContents = findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); break; } case DiagnosticCodes.ConsoleTypeDiagnostic: { - fileContents = this.findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); + fileContents = findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); break; } case DiagnosticCodes.ConfigPythonPathDiagnostic: { - fileContents = this.findAndReplace( + fileContents = findAndReplace(fileContents, '"pythonPath":', '"python":'); + fileContents = findAndReplace( fileContents, '{config:python.pythonPath}', - '{command:python.interpreterPath}' + '{command:python.interpreterPath}', ); - fileContents = this.findAndReplace( + fileContents = findAndReplace( fileContents, '{config:python.interpreterPath}', - '{command:python.interpreterPath}' + '{command:python.interpreterPath}', ); break; } @@ -188,11 +194,13 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { await this.fs.writeFile(launchJson, fileContents); } - private findAndReplace(fileContents: string, search: string, replace: string) { - const searchRegex = new RegExp(search, 'g'); - return fileContents.replace(searchRegex, replace); - } - private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { - return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); - } +} + +function findAndReplace(fileContents: string, search: string, replace: string) { + const searchRegex = new RegExp(search, 'g'); + return fileContents.replace(searchRegex, replace); +} + +function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { + return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); } diff --git a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts index 73f60ce7edfa..f08c09956838 100644 --- a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts @@ -1,20 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { DiagnosticSeverity, Uri, workspace as workspc, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../../common/application/types'; import '../../../common/extensions'; -import { traceError } from '../../../common/logger'; import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; -import { Diagnostics } from '../../../common/utils/localize'; +import { Common, Diagnostics } from '../../../common/utils/localize'; import { SystemVariables } from '../../../common/variables/systemVariables'; import { PythonPathSource } from '../../../debugger/extension/types'; import { IInterpreterHelper } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; +import { traceError } from '../../../logging'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; import { DiagnosticCodes } from '../constants'; @@ -24,22 +23,30 @@ import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, - IInvalidPythonPathInDebuggerService + IInvalidPythonPathInDebuggerService, } from '../types'; const messages = { - [DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic]: Diagnostics.invalidPythonPathInDebuggerSettings(), - [DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic]: Diagnostics.invalidPythonPathInDebuggerLaunch() + [DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic]: Diagnostics.invalidPythonPathInDebuggerSettings, + [DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic]: Diagnostics.invalidPythonPathInDebuggerLaunch, }; -export class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { +class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { constructor( code: | DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic | DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - resource: Resource + resource: Resource, ) { - super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource, 'always'); + super( + code, + messages[code], + DiagnosticSeverity.Error, + DiagnosticScope.WorkspaceFolder, + resource, + undefined, + 'always', + ); } } @@ -58,24 +65,31 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(IDiagnosticHandlerService) @named(DiagnosticCommandPromptHandlerServiceId) - protected readonly messageService: IDiagnosticHandlerService + protected readonly messageService: IDiagnosticHandlerService, ) { super( [ DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic + DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, ], serviceContainer, disposableRegistry, - true + true, ); } - public async diagnose(_resource: Resource): Promise { + + // eslint-disable-next-line class-methods-use-this + public async diagnose(): Promise { return []; } - public async validatePythonPath(pythonPath?: string, pythonPathSource?: PythonPathSource, resource?: Uri) { + + public async validatePythonPath( + pythonPath?: string, + pythonPathSource?: PythonPathSource, + resource?: Uri, + ): Promise { pythonPath = pythonPath ? this.resolveVariables(pythonPath, resource) : undefined; - // tslint:disable-next-line:no-invalid-template-strings + if (pythonPath === '${command:python.interpreterPath}' || !pythonPath) { pythonPath = this.configService.getSettings(resource).pythonPath; } @@ -87,8 +101,8 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService this.handle([ new InvalidPythonPathInDebuggerDiagnostic( DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, - resource - ) + resource, + ), ]) .catch((ex) => traceError('Failed to handle invalid python path in launch.json debugger', ex)) .ignoreErrors(); @@ -96,14 +110,15 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService this.handle([ new InvalidPythonPathInDebuggerDiagnostic( DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - resource - ) + resource, + ), ]) .catch((ex) => traceError('Failed to handle invalid python path in settings.json debugger', ex)) .ignoreErrors(); } return false; } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { @@ -114,36 +129,38 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService await this.messageService.handle(diagnostic, { commandPrompts }); } + protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { const systemVariables = new SystemVariables(resource, undefined, this.workspace); return systemVariables.resolveAny(pythonPath); } + private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { switch (diagnostic.code) { case DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic: { return [ { - prompt: 'Select Python Interpreter', + prompt: Common.selectPythonInterpreter, command: this.commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', - options: 'python.setInterpreter' - }) - } + options: 'python.setInterpreter', + }), + }, ]; } case DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic: { return [ { - prompt: 'Open launch.json', + prompt: Common.openLaunch, command: { diagnostic, invoke: async (): Promise => { - const launchJson = this.getLaunchJsonFile(workspc.workspaceFolders![0]); + const launchJson = getLaunchJsonFile(workspc.workspaceFolders![0]); const doc = await this.documentManager.openTextDocument(launchJson); await this.documentManager.showTextDocument(doc); - } - } - } + }, + }, + }, ]; } default: { @@ -151,7 +168,8 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService } } } - private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { - return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); - } +} + +function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { + return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); } diff --git a/src/client/application/diagnostics/checks/jediPython27NotSupported.ts b/src/client/application/diagnostics/checks/jediPython27NotSupported.ts new file mode 100644 index 000000000000..3d358325032e --- /dev/null +++ b/src/client/application/diagnostics/checks/jediPython27NotSupported.ts @@ -0,0 +1,108 @@ +/* eslint-disable max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, named } from 'inversify'; +import { ConfigurationTarget, DiagnosticSeverity } from 'vscode'; +import { LanguageServerType } from '../../../activation/types'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { Common, Python27Support } from '../../../common/utils/localize'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { IDiagnosticsCommandFactory } from '../commands/types'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +export class JediPython27NotSupportedDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.JediPython27NotSupportedDiagnostic, + message, + DiagnosticSeverity.Warning, + DiagnosticScope.Global, + resource, + ); + } +} + +export const JediPython27NotSupportedDiagnosticServiceId = 'JediPython27NotSupportedDiagnosticServiceId'; + +export class JediPython27NotSupportedDiagnosticService extends BaseDiagnosticsService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true); + } + + public async diagnose(resource: Resource): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const { languageServer } = this.configurationService.getSettings(resource); + + await this.updateLanguageServerSetting(resource); + + // We don't need to check for JediLSP here, because we retrieve the setting from the configuration service, + // Which already switched the JediLSP option to Jedi. + if (interpreter && (interpreter.version?.major ?? 0) < 3 && languageServer === LanguageServerType.Jedi) { + return [new JediPython27NotSupportedDiagnostic(Python27Support.jediMessage, resource)]; + } + + return []; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + const options = [ + { + prompt: Common.gotIt, + }, + { + prompt: Common.doNotShowAgain, + command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }), + }, + ]; + + await this.messageService.handle(diagnostic, { commandPrompts: options }); + } + + private async updateLanguageServerSetting(resource: Resource): Promise { + // Update settings.json value to Jedi if it's JediLSP. + const settings = this.workspaceService + .getConfiguration('python', resource) + .inspect('languageServer'); + + let configTarget: ConfigurationTarget; + + if (settings?.workspaceValue === LanguageServerType.JediLSP) { + configTarget = ConfigurationTarget.Workspace; + } else if (settings?.globalValue === LanguageServerType.JediLSP) { + configTarget = ConfigurationTarget.Global; + } else { + return; + } + + await this.configurationService.updateSetting( + 'languageServer', + LanguageServerType.Jedi, + resource, + configTarget, + ); + } +} diff --git a/src/client/application/diagnostics/checks/lsNotSupported.ts b/src/client/application/diagnostics/checks/lsNotSupported.ts deleted file mode 100644 index c33154fbbff7..000000000000 --- a/src/client/application/diagnostics/checks/lsNotSupported.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, named } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; -import { ILanguageServerCompatibilityService } from '../../../activation/types'; -import { IDisposableRegistry, Resource } from '../../../common/types'; -import { Diagnostics } from '../../../common/utils/localize'; -import { IServiceContainer } from '../../../ioc/types'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; -import { IDiagnosticsCommandFactory } from '../commands/types'; -import { DiagnosticCodes } from '../constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; - -export class LSNotSupportedDiagnostic extends BaseDiagnostic { - constructor(message: string, resource: Resource) { - super( - DiagnosticCodes.LSNotSupportedDiagnostic, - message, - DiagnosticSeverity.Warning, - DiagnosticScope.Global, - resource - ); - } -} - -export const LSNotSupportedDiagnosticServiceId = 'LSNotSupportedDiagnosticServiceId'; - -export class LSNotSupportedDiagnosticService extends BaseDiagnosticsService { - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(ILanguageServerCompatibilityService) - private readonly lsCompatibility: ILanguageServerCompatibilityService, - @inject(IDiagnosticHandlerService) - @named(DiagnosticCommandPromptHandlerServiceId) - protected readonly messageService: IDiagnosticHandlerService, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry - ) { - super([DiagnosticCodes.LSNotSupportedDiagnostic], serviceContainer, disposableRegistry, false); - } - public async diagnose(resource: Resource): Promise { - if (await this.lsCompatibility.isSupported()) { - return []; - } else { - return [new LSNotSupportedDiagnostic(Diagnostics.lsNotSupported(), resource)]; - } - } - protected async onHandle(diagnostics: IDiagnostic[]): Promise { - if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { - return; - } - const diagnostic = diagnostics[0]; - if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { - return; - } - const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - const options = [ - { - prompt: 'More Info', - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://aka.ms/pythonlsrequirements' - }) - }, - { - prompt: 'Do not show again', - command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) - } - ]; - - await this.messageService.handle(diagnostic, { commandPrompts: options }); - } -} diff --git a/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/src/client/application/diagnostics/checks/macPythonInterpreter.ts index 46933e4ae8f6..21d6b34fb7c5 100644 --- a/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -1,45 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, DiagnosticSeverity, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { DeprecatePythonPath } from '../../../common/experiments/groups'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; import { IPlatformService } from '../../../common/platform/types'; import { IConfigurationService, IDisposableRegistry, - IExperimentsManager, IInterpreterPathService, InterpreterConfigurationScope, - Resource + Resource, } from '../../../common/types'; -import { IInterpreterHelper, IInterpreterService } from '../../../interpreter/contracts'; +import { IInterpreterHelper } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; -import { InterpreterType } from '../../../pythonEnvironments/info'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; import { DiagnosticCodes } from '../constants'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from '../types'; +import { Common } from '../../../common/utils/localize'; const messages = { - [DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic]: - 'You have selected the macOS system install of Python, which is not recommended for use with the Python extension. Some functionality will be limited, please select a different interpreter.', - [DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic]: - 'The macOS system install of Python is not recommended, some functionality in the extension will be limited. Install another version of Python for the best experience.' + [DiagnosticCodes.MacInterpreterSelected]: l10n.t( + 'The selected macOS system install of Python is not recommended, some functionality in the extension will be limited. [Install another version of Python](https://www.python.org/downloads) or select a different interpreter for the best experience. [Learn more](https://aka.ms/AA7jfor).', + ), }; export class InvalidMacPythonInterpreterDiagnostic extends BaseDiagnostic { - constructor( - code: - | DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic - | DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - resource: Resource - ) { + constructor(code: DiagnosticCodes.MacInterpreterSelected, resource: Resource) { super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource); } } @@ -49,83 +39,45 @@ export const InvalidMacPythonInterpreterServiceId = 'InvalidMacPythonInterpreter @injectable() export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { protected changeThrottleTimeout = 1000; - private timeOut?: NodeJS.Timer | number; + + private timeOut?: NodeJS.Timeout | number; + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(IPlatformService) private readonly platform: IPlatformService, - @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, ) { - super( - [ - DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic - ], - serviceContainer, - disposableRegistry, - true - ); + super([DiagnosticCodes.MacInterpreterSelected], serviceContainer, disposableRegistry, true); this.addPythonPathChangedHandler(); } - public dispose() { - if (this.timeOut) { - // tslint:disable-next-line: no-any - clearTimeout(this.timeOut as any); + + public dispose(): void { + if (this.timeOut && typeof this.timeOut !== 'number') { + clearTimeout(this.timeOut); this.timeOut = undefined; } } + public async diagnose(resource: Resource): Promise { if (!this.platform.isMac) { return []; } const configurationService = this.serviceContainer.get(IConfigurationService); const settings = configurationService.getSettings(resource); - if (settings.disableInstallationChecks === true) { - return []; - } - - const hasInterpreters = await this.interpreterService.hasInterpreters; - if (!hasInterpreters) { + if (!(await this.helper.isMacDefaultPythonPath(settings.pythonPath))) { return []; } - - const currentInterpreter = await this.interpreterService.getActiveInterpreter(resource); - if (!currentInterpreter) { - return []; - } - - if (!this.helper.isMacDefaultPythonPath(settings.pythonPath)) { - return []; - } - if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { - return []; - } - - const interpreters = await this.interpreterService.getInterpreters(resource); - if (interpreters.filter((i) => !this.helper.isMacDefaultPythonPath(i.path)).length === 0) { - return [ - new InvalidMacPythonInterpreterDiagnostic( - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, - resource - ) - ]; - } - - return [ - new InvalidMacPythonInterpreterDiagnostic( - DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - resource - ) - ]; + return [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelected, resource)]; } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { if (diagnostics.length === 0) { return; } const messageService = this.serviceContainer.get>( IDiagnosticHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); await Promise.all( diagnostics.map(async (diagnostic) => { @@ -135,49 +87,24 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { return; } const commandPrompts = this.getCommandPrompts(diagnostic); - return messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); - }) + await messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); + }), ); } - protected addPythonPathChangedHandler() { - const workspaceService = this.serviceContainer.get(IWorkspaceService); + + protected addPythonPathChangedHandler(): void { const disposables = this.serviceContainer.get(IDisposableRegistry); const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); - const experiments = this.serviceContainer.get(IExperimentsManager); - if (experiments.inExperiment(DeprecatePythonPath.experiment)) { - disposables.push(interpreterPathService.onDidChange((i) => this.onDidChangeConfiguration(undefined, i))); - } - experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - disposables.push(workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + disposables.push(interpreterPathService.onDidChange((i) => this.onDidChangeConfiguration(i))); } + protected async onDidChangeConfiguration( - event?: ConfigurationChangeEvent, - interpreterConfigurationScope?: InterpreterConfigurationScope - ) { - let workspaceUri: Resource; - if (event) { - const workspaceService = this.serviceContainer.get(IWorkspaceService); - const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders - ? workspaceService.workspaceFolders!.map((workspace) => workspace.uri) - : [undefined]; - const workspaceUriIndex = workspacesUris.findIndex((uri) => - event.affectsConfiguration('python.pythonPath', uri) - ); - if (workspaceUriIndex === -1) { - return; - } - workspaceUri = workspacesUris[workspaceUriIndex]; - } else if (interpreterConfigurationScope) { - workspaceUri = interpreterConfigurationScope.uri; - } else { - throw new Error( - 'One of `interpreterConfigurationScope` or `event` should be defined when calling `onDidChangeConfiguration`.' - ); - } + interpreterConfigurationScope: InterpreterConfigurationScope, + ): Promise { + const workspaceUri = interpreterConfigurationScope.uri; // Lets wait, for more changes, dirty simple throttling. - if (this.timeOut) { - // tslint:disable-next-line: no-any - clearTimeout(this.timeOut as any); + if (this.timeOut && typeof this.timeOut !== 'number') { + clearTimeout(this.timeOut); this.timeOut = undefined; } this.timeOut = setTimeout(() => { @@ -187,50 +114,26 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { .ignoreErrors(); }, this.changeThrottleTimeout); } + private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); switch (diagnostic.code) { - case DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic: { + case DiagnosticCodes.MacInterpreterSelected: { return [ { - prompt: 'Select Python Interpreter', + prompt: Common.selectPythonInterpreter, command: commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', - options: 'python.setInterpreter' - }) + options: 'python.setInterpreter', + }), }, { - prompt: 'Do not show again', + prompt: Common.doNotShowAgain, command: commandFactory.createCommand(diagnostic, { type: 'ignore', - options: DiagnosticScope.Global - }) - } - ]; - } - case DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic: { - return [ - { - prompt: 'Learn more', - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' - }) - }, - { - prompt: 'Download', - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://www.python.org/downloads' - }) + options: DiagnosticScope.Global, + }), }, - { - prompt: 'Do not show again', - command: commandFactory.createCommand(diagnostic, { - type: 'ignore', - options: DiagnosticScope.Global - }) - } ]; } default: { diff --git a/src/client/application/diagnostics/checks/powerShellActivation.ts b/src/client/application/diagnostics/checks/powerShellActivation.ts index a2e8c08ff5c5..85f68db0d6a4 100644 --- a/src/client/application/diagnostics/checks/powerShellActivation.ts +++ b/src/client/application/diagnostics/checks/powerShellActivation.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import { traceError } from '../../../common/logger'; import { useCommandPromptAsDefaultShell } from '../../../common/terminal/commandPrompt'; import { IConfigurationService, ICurrentProcess, IDisposableRegistry, Resource } from '../../../common/types'; +import { Common } from '../../../common/utils/localize'; import { IServiceContainer } from '../../../ioc/types'; +import { traceError } from '../../../logging'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; @@ -18,8 +18,9 @@ import { DiagnosticCodes } from '../constants'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; -const PowershellActivationNotSupportedWithBatchFilesMessage = - 'Activation of the selected Python environment is not supported in PowerShell. Consider changing your shell to Command Prompt.'; +const PowershellActivationNotSupportedWithBatchFilesMessage = l10n.t( + 'Activation of the selected Python environment is not supported in PowerShell. Consider changing your shell to Command Prompt.', +); export class PowershellActivationNotAvailableDiagnostic extends BaseDiagnostic { constructor(resource: Resource) { @@ -29,7 +30,8 @@ export class PowershellActivationNotAvailableDiagnostic extends BaseDiagnostic { DiagnosticSeverity.Warning, DiagnosticScope.Global, resource, - 'always' + undefined, + 'always', ); } } @@ -40,24 +42,28 @@ export const PowerShellActivationHackDiagnosticsServiceId = @injectable() export class PowerShellActivationHackDiagnosticsService extends BaseDiagnosticsService { protected readonly messageService: IDiagnosticHandlerService; + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { super( [DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic], serviceContainer, disposableRegistry, - true + true, ); this.messageService = serviceContainer.get>( IDiagnosticHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); } - public async diagnose(_resource: Resource): Promise { + + // eslint-disable-next-line class-methods-use-this + public async diagnose(): Promise { return []; } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { @@ -72,34 +78,34 @@ export class PowerShellActivationHackDiagnosticsService extends BaseDiagnosticsS const configurationService = this.serviceContainer.get(IConfigurationService); const options = [ { - prompt: 'Use Command Prompt', - // tslint:disable-next-line:no-object-literal-type-assertion + prompt: Common.useCommandPrompt, + command: { diagnostic, invoke: async (): Promise => { sendTelemetryEvent(EventName.DIAGNOSTICS_ACTION, undefined, { - action: 'switchToCommandPrompt' + action: 'switchToCommandPrompt', }); useCommandPromptAsDefaultShell(currentProcess, configurationService).catch((ex) => - traceError('Use Command Prompt as default shell', ex) + traceError('Use Command Prompt as default shell', ex), ); - } - } + }, + }, }, { - prompt: 'Ignore' + prompt: Common.ignore, }, { - prompt: 'Always Ignore', - command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) + prompt: Common.alwaysIgnore, + command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }), }, { - prompt: 'More Info', + prompt: Common.moreInfo, command: commandFactory.createCommand(diagnostic, { type: 'launch', - options: 'https://aka.ms/CondaPwsh' - }) - } + options: 'https://aka.ms/CondaPwsh', + }), + }, ]; await this.messageService.handle(diagnostic, { commandPrompts: options }); diff --git a/src/client/application/diagnostics/checks/pylanceDefault.ts b/src/client/application/diagnostics/checks/pylanceDefault.ts new file mode 100644 index 000000000000..16ee2968c8d6 --- /dev/null +++ b/src/client/application/diagnostics/checks/pylanceDefault.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { inject, named } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { IDisposableRegistry, IExtensionContext, Resource } from '../../../common/types'; +import { Diagnostics, Common } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +export const PYLANCE_PROMPT_MEMENTO = 'pylanceDefaultPromptMemento'; +const EXTENSION_VERSION_MEMENTO = 'extensionVersion'; + +export class PylanceDefaultDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.PylanceDefaultDiagnostic, + message, + DiagnosticSeverity.Information, + DiagnosticScope.Global, + resource, + ); + } +} + +export const PylanceDefaultDiagnosticServiceId = 'PylanceDefaultDiagnosticServiceId'; + +export class PylanceDefaultDiagnosticService extends BaseDiagnosticsService { + public initialMementoValue: string | undefined = undefined; + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.PylanceDefaultDiagnostic], serviceContainer, disposableRegistry, true, true); + + this.initialMementoValue = this.context.globalState.get(EXTENSION_VERSION_MEMENTO); + } + + public async diagnose(resource: Resource): Promise { + if (!(await this.shouldShowPrompt())) { + return []; + } + + return [new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage, resource)]; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + const options = [{ prompt: Common.ok }]; + + await this.messageService.handle(diagnostic, { + commandPrompts: options, + onClose: this.updateMemento.bind(this), + }); + } + + private async updateMemento() { + await this.context.globalState.update(PYLANCE_PROMPT_MEMENTO, true); + } + + private async shouldShowPrompt(): Promise { + const savedVersion: string | undefined = this.initialMementoValue; + const promptShown: boolean | undefined = this.context.globalState.get(PYLANCE_PROMPT_MEMENTO); + + // savedVersion being undefined means that this is the first time the user activates the extension, + // and we don't want to show the prompt to first-time users. + // We set PYLANCE_PROMPT_MEMENTO here to skip the prompt + // in case the user reloads the extension and savedVersion becomes set + if (savedVersion === undefined) { + await this.updateMemento(); + return false; + } + + // promptShown being undefined means that this is the first time we check if we should show the prompt. + return promptShown === undefined; + } +} diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index 6040234f7f2a..9167e232a417 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -1,16 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import * as path from 'path'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService, Resource } from '../../../common/types'; import { IInterpreterService } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; import { DiagnosticCodes } from '../constants'; @@ -20,85 +18,245 @@ import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, - IDiagnosticMessageOnCloseHandler + IDiagnosticMessageOnCloseHandler, } from '../types'; +import { Common, Interpreters } from '../../../common/utils/localize'; +import { Commands } from '../../../common/constants'; +import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { cache } from '../../../common/utils/decorators'; +import { noop } from '../../../common/utils/misc'; +import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { IFileSystem } from '../../../common/platform/types'; +import { traceError, traceWarn } from '../../../logging'; +import { getExecutable } from '../../../common/process/internal/python'; +import { getSearchPathEnvVarNames } from '../../../common/utils/exec'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { normCasePath } from '../../../common/platform/fs-paths'; +import { useEnvExtension } from '../../../envExt/api.internal'; const messages = { - [DiagnosticCodes.NoPythonInterpretersDiagnostic]: - 'Python is not installed. Please download and install Python before using the extension.', - [DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic]: - 'No Python interpreter is selected. You need to select a Python interpreter to enable features such as IntelliSense, linting, and debugging.' + [DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t( + 'No Python interpreter is selected. Please select a Python interpreter to enable features such as IntelliSense, linting, and debugging.', + ), + [DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: l10n.t( + 'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging. See output for more details regarding why the interpreter is invalid.', + ), + [DiagnosticCodes.InvalidComspecDiagnostic]: l10n.t( + 'We detected an issue with one of your environment variables that breaks features such as IntelliSense, linting and debugging. Try setting the "ComSpec" variable to a valid Command Prompt path in your system to fix it.', + ), + [DiagnosticCodes.IncompletePathVarDiagnostic]: l10n.t( + 'We detected an issue with "Path" environment variable that breaks features such as IntelliSense, linting and debugging. Please edit it to make sure it contains the "System32" subdirectories.', + ), + [DiagnosticCodes.DefaultShellErrorDiagnostic]: l10n.t( + 'We detected an issue with your default shell that breaks features such as IntelliSense, linting and debugging. Try resetting "ComSpec" and "Path" environment variables to fix it.', + ), }; export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic { constructor( - code: - | DiagnosticCodes.NoPythonInterpretersDiagnostic - | DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - resource: Resource + code: DiagnosticCodes.NoPythonInterpretersDiagnostic | DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + resource: Resource, + workspaceService: IWorkspaceService, + scope = DiagnosticScope.WorkspaceFolder, ) { - super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource); + let formatArg = ''; + if ( + workspaceService.workspaceFile && + workspaceService.workspaceFolders && + workspaceService.workspaceFolders?.length > 1 + ) { + // Specify folder name in case of multiroot scenarios + const folder = workspaceService.getWorkspaceFolder(resource); + if (folder) { + formatArg = ` ${l10n.t('for workspace')} ${path.basename(folder.uri.fsPath)}`; + } + } + super(code, messages[code].format(formatArg), DiagnosticSeverity.Error, scope, resource, undefined, 'always'); + } +} + +type DefaultShellDiagnostics = + | DiagnosticCodes.InvalidComspecDiagnostic + | DiagnosticCodes.IncompletePathVarDiagnostic + | DiagnosticCodes.DefaultShellErrorDiagnostic; + +export class DefaultShellDiagnostic extends BaseDiagnostic { + constructor(code: DefaultShellDiagnostics, resource: Resource, scope = DiagnosticScope.Global) { + super(code, messages[code], DiagnosticSeverity.Error, scope, resource, undefined, 'always'); } } export const InvalidPythonInterpreterServiceId = 'InvalidPythonInterpreterServiceId'; @injectable() -export class InvalidPythonInterpreterService extends BaseDiagnosticsService { +export class InvalidPythonInterpreterService extends BaseDiagnosticsService + implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { super( [ DiagnosticCodes.NoPythonInterpretersDiagnostic, - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidComspecDiagnostic, + DiagnosticCodes.IncompletePathVarDiagnostic, + DiagnosticCodes.DefaultShellErrorDiagnostic, ], serviceContainer, disposableRegistry, - false + false, + ); + } + + public async activate(): Promise { + const commandManager = this.serviceContainer.get(ICommandManager); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.TriggerEnvironmentSelection, (resource: Resource) => + this.triggerEnvSelectionIfNecessary(resource), + ), + ); + const interpreterService = this.serviceContainer.get(IInterpreterService); + this.disposableRegistry.push( + interpreterService.onDidChangeInterpreterConfiguration((e) => + commandManager.executeCommand(Commands.TriggerEnvironmentSelection, e).then(noop, noop), + ), ); } + public async diagnose(resource: Resource): Promise { - const configurationService = this.serviceContainer.get(IConfigurationService); - const settings = configurationService.getSettings(resource); - if (settings.disableInstallationChecks === true) { - return []; - } + return this.diagnoseDefaultShell(resource); + } + public async _manualDiagnose(resource: Resource): Promise { + const workspaceService = this.serviceContainer.get(IWorkspaceService); const interpreterService = this.serviceContainer.get(IInterpreterService); - // hasInterpreters being false can mean one of 2 things: - // 1. getInterpreters hasn't returned any interpreters; - // 2. getInterpreters hasn't run yet. - // We want to make sure that false comes from 1, so we're adding this fix until we refactor interpreter discovery. - // Also see https://github.com/microsoft/vscode-python/issues/3023. - const hasInterpreters = - (await interpreterService.hasInterpreters) || - (await interpreterService.getInterpreters(resource)).length > 0; - - if (!hasInterpreters) { - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic, resource)]; + const diagnostics = await this.diagnoseDefaultShell(resource); + if (diagnostics.length > 0) { + return diagnostics; + } + const hasInterpreters = await interpreterService.hasInterpreters(); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const isInterpreterSetToDefault = interpreterPathService.get(resource) === 'python'; + + if (!hasInterpreters && isInterpreterSetToDefault) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } + return [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + resource, + workspaceService, + DiagnosticScope.Global, + ), + ]; } const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtNoActiveEnvironment); + } return [ new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - resource - ) + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + resource, + workspaceService, + ), ]; } + return []; + } + + public async triggerEnvSelectionIfNecessary(resource: Resource): Promise { + const diagnostics = await this._manualDiagnose(resource); + if (!diagnostics.length) { + return true; + } + this.handle(diagnostics).ignoreErrors(); + return false; + } + private async diagnoseDefaultShell(resource: Resource): Promise { + if (getOSType() !== OSType.Windows) { + return []; + } + const interpreterService = this.serviceContainer.get(IInterpreterService); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); + if (currentInterpreter) { + return []; + } + try { + await this.shellExecPython(); + } catch (ex) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((ex as any).errno === -4058) { + // ENOENT (-4058) error is thrown by Node when the default shell is invalid. + traceError('ComSpec is likely set to an invalid value', getEnvironmentVariable('ComSpec')); + if (await this.isComspecInvalid()) { + return [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, resource)]; + } + if (this.isPathVarIncomplete()) { + traceError('PATH env var appears to be incomplete', process.env.Path, process.env.PATH); + return [new DefaultShellDiagnostic(DiagnosticCodes.IncompletePathVarDiagnostic, resource)]; + } + return [new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, resource)]; + } + } return []; } + + private async isComspecInvalid() { + const comSpec = getEnvironmentVariable('ComSpec') ?? ''; + const fs = this.serviceContainer.get(IFileSystem); + return fs.fileExists(comSpec).then((exists) => !exists); + } + + // eslint-disable-next-line class-methods-use-this + private isPathVarIncomplete() { + const envVars = getSearchPathEnvVarNames(); + const systemRoot = getEnvironmentVariable('SystemRoot') ?? 'C:\\WINDOWS'; + const system32 = path.join(systemRoot, 'system32'); + for (const envVar of envVars) { + const value = getEnvironmentVariable(envVar); + if (value && normCasePath(value).includes(normCasePath(system32))) { + return false; + } + } + return true; + } + + @cache(-1, true) + // eslint-disable-next-line class-methods-use-this + private async shellExecPython() { + const configurationService = this.serviceContainer.get(IConfigurationService); + const { pythonPath } = configurationService.getSettings(); + const [args] = getExecutable(); + const argv = [pythonPath, ...args]; + // Concat these together to make a set of quoted strings + const quoted = argv.reduce( + (p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); + const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + const service = await processServiceFactory.create(); + return service.shellExec(quoted, { timeout: 15000 }); + } + + @cache(1000, true) // This is to handle throttling of multiple events. protected async onHandle(diagnostics: IDiagnostic[]): Promise { if (diagnostics.length === 0) { return; } const messageService = this.serviceContainer.get>( IDiagnosticHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); await Promise.all( diagnostics.map(async (diagnostic) => { @@ -106,50 +264,63 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService { return; } const commandPrompts = this.getCommandPrompts(diagnostic); - const onClose = this.getOnCloseHandler(diagnostic); - return messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message, onClose }); - }) + const onClose = getOnCloseHandler(diagnostic); + await messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message, onClose }); + }), ); } + private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - switch (diagnostic.code) { - case DiagnosticCodes.NoPythonInterpretersDiagnostic: { - return [ - { - prompt: 'Download', - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://www.python.org/downloads' - }) - } - ]; - } - case DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic: { - return [ - { - prompt: 'Select Python Interpreter', - command: commandFactory.createCommand(diagnostic, { - type: 'executeVSCCommand', - options: 'python.setInterpreter' - }) - } - ]; - } - default: { - throw new Error("Invalid diagnostic for 'InvalidPythonInterpreterService'"); - } - } - } - private getOnCloseHandler(diagnostic: IDiagnostic): IDiagnosticMessageOnCloseHandler | undefined { - if (diagnostic.code === DiagnosticCodes.NoPythonInterpretersDiagnostic) { - return (response?: string) => { - sendTelemetryEvent(EventName.PYTHON_NOT_INSTALLED_PROMPT, undefined, { - selection: response ? 'Download' : 'Ignore' - }); + if ( + diagnostic.code === DiagnosticCodes.InvalidComspecDiagnostic || + diagnostic.code === DiagnosticCodes.IncompletePathVarDiagnostic || + diagnostic.code === DiagnosticCodes.DefaultShellErrorDiagnostic + ) { + const links: Record = { + InvalidComspecDiagnostic: 'https://aka.ms/AAk3djo', + IncompletePathVarDiagnostic: 'https://aka.ms/AAk744c', + DefaultShellErrorDiagnostic: 'https://aka.ms/AAk7qix', }; + return [ + { + prompt: Common.seeInstructions, + command: commandFactory.createCommand(diagnostic, { + type: 'launch', + options: links[diagnostic.code], + }), + }, + ]; + } + const prompts = [ + { + prompt: Common.selectPythonInterpreter, + command: commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: Commands.Set_Interpreter, + }), + }, + ]; + if (diagnostic.code === DiagnosticCodes.InvalidPythonInterpreterDiagnostic) { + prompts.push({ + prompt: Common.openOutputPanel, + command: commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: Commands.ViewOutput, + }), + }); } + return prompts; + } +} - return; +function getOnCloseHandler(diagnostic: IDiagnostic): IDiagnosticMessageOnCloseHandler | undefined { + if (diagnostic.code === DiagnosticCodes.NoPythonInterpretersDiagnostic) { + return (response?: string) => { + sendTelemetryEvent(EventName.PYTHON_NOT_INSTALLED_PROMPT, undefined, { + selection: response ? 'Download' : 'Ignore', + }); + }; } + return undefined; } diff --git a/src/client/application/diagnostics/checks/pythonPathDeprecated.ts b/src/client/application/diagnostics/checks/pythonPathDeprecated.ts deleted file mode 100644 index 305be378d831..000000000000 --- a/src/client/application/diagnostics/checks/pythonPathDeprecated.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, named } from 'inversify'; -import { ConfigurationTarget, DiagnosticSeverity } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../../common/constants'; -import { DeprecatePythonPath } from '../../../common/experiments/groups'; -import { IDisposableRegistry, IExperimentsManager, IOutputChannel, Resource } from '../../../common/types'; -import { Common, Diagnostics } from '../../../common/utils/localize'; -import { IServiceContainer } from '../../../ioc/types'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; -import { IDiagnosticsCommandFactory } from '../commands/types'; -import { DiagnosticCodes } from '../constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; - -export class PythonPathDeprecatedDiagnostic extends BaseDiagnostic { - constructor(message: string, resource: Resource) { - super( - DiagnosticCodes.PythonPathDeprecatedDiagnostic, - message, - DiagnosticSeverity.Information, - DiagnosticScope.WorkspaceFolder, - resource - ); - } -} - -export const PythonPathDeprecatedDiagnosticServiceId = 'PythonPathDeprecatedDiagnosticServiceId'; - -export class PythonPathDeprecatedDiagnosticService extends BaseDiagnosticsService { - private workspaceService: IWorkspaceService; - private output: IOutputChannel; - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDiagnosticHandlerService) - @named(DiagnosticCommandPromptHandlerServiceId) - protected readonly messageService: IDiagnosticHandlerService, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry - ) { - super([DiagnosticCodes.PythonPathDeprecatedDiagnostic], serviceContainer, disposableRegistry, true); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - } - public async diagnose(resource: Resource): Promise { - const experiments = this.serviceContainer.get(IExperimentsManager); - experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - if (!experiments.inExperiment(DeprecatePythonPath.experiment)) { - return []; - } - const setting = this.workspaceService.getConfiguration('python', resource).inspect('pythonPath'); - if (!setting) { - return []; - } - const isCodeWorkspaceSettingSet = this.workspaceService.workspaceFile && setting.workspaceValue !== undefined; - const isSettingsJsonSettingSet = setting.workspaceFolderValue !== undefined; - if (isCodeWorkspaceSettingSet || isSettingsJsonSettingSet) { - return [new PythonPathDeprecatedDiagnostic(Diagnostics.removedPythonPathFromSettings(), resource)]; - } - return []; - } - - public async _removePythonPathFromWorkspaceSettings(resource: Resource) { - const workspaceConfig = this.workspaceService.getConfiguration('python', resource); - await Promise.all([ - workspaceConfig.update('pythonPath', undefined, ConfigurationTarget.Workspace), - workspaceConfig.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder) - ]); - } - - protected async onHandle(diagnostics: IDiagnostic[]): Promise { - if (diagnostics.length === 0 || !(await this.canHandle(diagnostics[0]))) { - return; - } - const diagnostic = diagnostics[0]; - if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { - return; - } - await this._removePythonPathFromWorkspaceSettings(diagnostic.resource); - const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - const options = [ - { - prompt: Common.openOutputPanel(), - command: { - diagnostic, - invoke: async (): Promise => this.output.show(true) - } - }, - { - prompt: Common.doNotShowAgain(), - command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) - } - ]; - - await this.messageService.handle(diagnostic, { commandPrompts: options }); - } -} diff --git a/src/client/application/diagnostics/checks/switchToDefaultLS.ts b/src/client/application/diagnostics/checks/switchToDefaultLS.ts new file mode 100644 index 000000000000..bd93a684d9a2 --- /dev/null +++ b/src/client/application/diagnostics/checks/switchToDefaultLS.ts @@ -0,0 +1,80 @@ +/* eslint-disable max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { ConfigurationTarget, DiagnosticSeverity } from 'vscode'; +import { LanguageServerType } from '../../../activation/types'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IDisposableRegistry, Resource } from '../../../common/types'; +import { Common, SwitchToDefaultLS } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +export class SwitchToDefaultLanguageServerDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.SwitchToDefaultLanguageServerDiagnostic, + message, + DiagnosticSeverity.Warning, + DiagnosticScope.Global, + resource, + ); + } +} + +export const SwitchToDefaultLanguageServerDiagnosticServiceId = 'SwitchToDefaultLanguageServerDiagnosticServiceId'; + +@injectable() +export class SwitchToDefaultLanguageServerDiagnosticService extends BaseDiagnosticsService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true, true); + } + + public diagnose(resource: Resource): Promise { + let changed = false; + const config = this.workspaceService.getConfiguration('python'); + const value = config.inspect('languageServer'); + if (value?.workspaceValue === LanguageServerType.Microsoft) { + config.update('languageServer', 'Default', ConfigurationTarget.Workspace); + changed = true; + } + + if (value?.globalValue === LanguageServerType.Microsoft) { + config.update('languageServer', 'Default', ConfigurationTarget.Global); + changed = true; + } + + return Promise.resolve( + changed ? [new SwitchToDefaultLanguageServerDiagnostic(SwitchToDefaultLS.bannerMessage, resource)] : [], + ); + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + await this.messageService.handle(diagnostic, { + commandPrompts: [ + { + prompt: Common.gotIt, + }, + ], + }); + } +} diff --git a/src/client/application/diagnostics/checks/upgradeCodeRunner.ts b/src/client/application/diagnostics/checks/upgradeCodeRunner.ts deleted file mode 100644 index 9285b41d72e7..000000000000 --- a/src/client/application/diagnostics/checks/upgradeCodeRunner.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, named } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { CODE_RUNNER_EXTENSION_ID } from '../../../common/constants'; -import { DeprecatePythonPath } from '../../../common/experiments/groups'; -import { IDisposableRegistry, IExperimentsManager, IExtensions, Resource } from '../../../common/types'; -import { Common, Diagnostics } from '../../../common/utils/localize'; -import { IServiceContainer } from '../../../ioc/types'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; -import { IDiagnosticsCommandFactory } from '../commands/types'; -import { DiagnosticCodes } from '../constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; - -export class UpgradeCodeRunnerDiagnostic extends BaseDiagnostic { - constructor(message: string, resource: Resource) { - super( - DiagnosticCodes.UpgradeCodeRunnerDiagnostic, - message, - DiagnosticSeverity.Information, - DiagnosticScope.Global, - resource - ); - } -} - -export const UpgradeCodeRunnerDiagnosticServiceId = 'UpgradeCodeRunnerDiagnosticServiceId'; - -export class UpgradeCodeRunnerDiagnosticService extends BaseDiagnosticsService { - public _diagnosticReturned: boolean = false; - private workspaceService: IWorkspaceService; - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDiagnosticHandlerService) - @named(DiagnosticCommandPromptHandlerServiceId) - protected readonly messageService: IDiagnosticHandlerService, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IExtensions) private readonly extensions: IExtensions - ) { - super([DiagnosticCodes.UpgradeCodeRunnerDiagnostic], serviceContainer, disposableRegistry, true); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - } - public async diagnose(resource: Resource): Promise { - if (this._diagnosticReturned) { - return []; - } - const experiments = this.serviceContainer.get(IExperimentsManager); - experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - if (!experiments.inExperiment(DeprecatePythonPath.experiment)) { - return []; - } - const extension = this.extensions.getExtension(CODE_RUNNER_EXTENSION_ID); - if (!extension) { - return []; - } - // Available feature flags: https://github.com/formulahendry/vscode-code-runner/blob/master/package.json#L6 - const flagValue: boolean | undefined = extension.packageJSON?.featureFlags?.usingNewPythonInterpreterPathApiV2; - if (flagValue) { - // Using new version of Code runner already, no need to upgrade - return []; - } - const pythonExecutor = this.workspaceService - .getConfiguration('code-runner', resource) - .get('executorMap.python'); - if (pythonExecutor?.includes('$pythonPath')) { - this._diagnosticReturned = true; - return [new UpgradeCodeRunnerDiagnostic(Diagnostics.upgradeCodeRunner(), resource)]; - } - return []; - } - - protected async onHandle(diagnostics: IDiagnostic[]): Promise { - if (diagnostics.length === 0 || !(await this.canHandle(diagnostics[0]))) { - return; - } - const diagnostic = diagnostics[0]; - if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { - return; - } - const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - const options = [ - { - prompt: Common.doNotShowAgain(), - command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) - } - ]; - - await this.messageService.handle(diagnostic, { commandPrompts: options }); - } -} diff --git a/src/client/application/diagnostics/commands/execVSCCommand.ts b/src/client/application/diagnostics/commands/execVSCCommand.ts index cb935f05d33d..50c7367f199a 100644 --- a/src/client/application/diagnostics/commands/execVSCCommand.ts +++ b/src/client/application/diagnostics/commands/execVSCCommand.ts @@ -15,7 +15,7 @@ export class ExecuteVSCCommand extends BaseDiagnosticCommand { constructor( diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, - private commandName: CommandsWithoutArgs + private commandName: CommandsWithoutArgs, ) { super(diagnostic); } diff --git a/src/client/application/diagnostics/commands/ignore.ts b/src/client/application/diagnostics/commands/ignore.ts index 07078cc17fe6..311128195975 100644 --- a/src/client/application/diagnostics/commands/ignore.ts +++ b/src/client/application/diagnostics/commands/ignore.ts @@ -13,7 +13,7 @@ export class IgnoreDiagnosticCommand extends BaseDiagnosticCommand { constructor( diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, - private readonly scope: DiagnosticScope + private readonly scope: DiagnosticScope, ) { super(diagnostic); } diff --git a/src/client/application/diagnostics/commands/types.ts b/src/client/application/diagnostics/commands/types.ts index 118dc626be82..f65460b0d113 100644 --- a/src/client/application/diagnostics/commands/types.ts +++ b/src/client/application/diagnostics/commands/types.ts @@ -7,10 +7,10 @@ import { CommandsWithoutArgs } from '../../../common/application/commands'; import { DiagnosticScope, IDiagnostic, IDiagnosticCommand } from '../types'; export type CommandOption = { type: Type; options: Option }; -export type LaunchBrowserOption = CommandOption<'launch', string>; -export type IgnoreDiagnostOption = CommandOption<'ignore', DiagnosticScope>; -export type ExecuteVSCCommandOption = CommandOption<'executeVSCCommand', CommandsWithoutArgs>; -export type CommandOptions = LaunchBrowserOption | IgnoreDiagnostOption | ExecuteVSCCommandOption; +type LaunchBrowserOption = CommandOption<'launch', string>; +type IgnoreDiagnosticOption = CommandOption<'ignore', DiagnosticScope>; +type ExecuteVSCCommandOption = CommandOption<'executeVSCCommand', CommandsWithoutArgs>; +export type CommandOptions = LaunchBrowserOption | IgnoreDiagnosticOption | ExecuteVSCCommandOption; export const IDiagnosticsCommandFactory = Symbol('IDiagnosticsCommandFactory'); diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index b975ae9222a0..ca2867fc4f49 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -7,16 +7,21 @@ export enum DiagnosticCodes { InvalidEnvironmentPathVariableDiagnostic = 'InvalidEnvironmentPathVariableDiagnostic', InvalidDebuggerTypeDiagnostic = 'InvalidDebuggerTypeDiagnostic', NoPythonInterpretersDiagnostic = 'NoPythonInterpretersDiagnostic', - MacInterpreterSelectedAndNoOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndNoOtherInterpretersDiagnostic', - MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic', + MacInterpreterSelected = 'MacInterpreterSelected', InvalidPythonPathInDebuggerSettingsDiagnostic = 'InvalidPythonPathInDebuggerSettingsDiagnostic', InvalidPythonPathInDebuggerLaunchDiagnostic = 'InvalidPythonPathInDebuggerLaunchDiagnostic', EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic', - NoCurrentlySelectedPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', + InvalidPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', + InvalidComspecDiagnostic = 'InvalidComspecDiagnostic', + IncompletePathVarDiagnostic = 'IncompletePathVarDiagnostic', + DefaultShellErrorDiagnostic = 'DefaultShellErrorDiagnostic', LSNotSupportedDiagnostic = 'LSNotSupportedDiagnostic', PythonPathDeprecatedDiagnostic = 'PythonPathDeprecatedDiagnostic', JustMyCodeDiagnostic = 'JustMyCodeDiagnostic', ConsoleTypeDiagnostic = 'ConsoleTypeDiagnostic', ConfigPythonPathDiagnostic = 'ConfigPythonPathDiagnostic', - UpgradeCodeRunnerDiagnostic = 'UpgradeCodeRunnerDiagnostic' + PylanceDefaultDiagnostic = 'PylanceDefaultDiagnostic', + JediPython27NotSupportedDiagnostic = 'JediPython27NotSupportedDiagnostic', + SwitchToDefaultLanguageServerDiagnostic = 'SwitchToDefaultLanguageServerDiagnostic', + SwitchToPreReleaseExtensionDiagnostic = 'SwitchToPreReleaseExtensionDiagnostic', } diff --git a/src/client/application/diagnostics/filter.ts b/src/client/application/diagnostics/filter.ts index 7aaf94f490e3..a304a6f558fc 100644 --- a/src/client/application/diagnostics/filter.ts +++ b/src/client/application/diagnostics/filter.ts @@ -10,7 +10,7 @@ import { DiagnosticScope, IDiagnosticFilterService } from './types'; export enum FilterKeys { GlobalDiagnosticFilter = 'GLOBAL_DIAGNOSTICS_FILTER', - WorkspaceDiagnosticFilter = 'WORKSPACE_DIAGNOSTICS_FILTER' + WorkspaceDiagnosticFilter = 'WORKSPACE_DIAGNOSTICS_FILTER', } @injectable() @@ -21,7 +21,7 @@ export class DiagnosticFilterService implements IDiagnosticFilterService { const globalState = factory.createGlobalPersistentState(FilterKeys.GlobalDiagnosticFilter, []); const workspaceState = factory.createWorkspacePersistentState( FilterKeys.WorkspaceDiagnosticFilter, - [] + [], ); return globalState.value.indexOf(code) >= 0 || workspaceState.value.indexOf(code) >= 0; } diff --git a/src/client/application/diagnostics/promptHandler.ts b/src/client/application/diagnostics/promptHandler.ts index 210a7c5ecee3..25b946b2ffb5 100644 --- a/src/client/application/diagnostics/promptHandler.ts +++ b/src/client/application/diagnostics/promptHandler.ts @@ -28,13 +28,13 @@ export class DiagnosticCommandPromptHandlerService implements IDiagnosticHandler } public async handle( diagnostic: IDiagnostic, - options: MessageCommandPrompt = { commandPrompts: [] } + options: MessageCommandPrompt = { commandPrompts: [] }, ): Promise { const prompts = options.commandPrompts.map((option) => option.prompt); const response = await this.displayMessage( options.message ? options.message : diagnostic.message, diagnostic.severity, - prompts + prompts, ); if (options.onClose) { options.onClose(response); @@ -50,7 +50,7 @@ export class DiagnosticCommandPromptHandlerService implements IDiagnosticHandler private async displayMessage( message: string, severity: DiagnosticSeverity, - prompts: string[] + prompts: string[], ): Promise { switch (severity) { case DiagnosticSeverity.Error: { diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index 9aefb8e742d9..acf460b88625 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -3,103 +3,101 @@ 'use strict'; -import { LanguageServerType } from '../../activation/types'; +import { IExtensionSingleActivationService } from '../../activation/types'; import { IServiceManager } from '../../ioc/types'; import { IApplicationDiagnostics } from '../types'; import { ApplicationDiagnostics } from './applicationDiagnostics'; import { EnvironmentPathVariableDiagnosticsService, - EnvironmentPathVariableDiagnosticsServiceId + EnvironmentPathVariableDiagnosticsServiceId, } from './checks/envPathVariable'; -import { - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId -} from './checks/invalidLaunchJsonDebugger'; import { InvalidPythonPathInDebuggerService, - InvalidPythonPathInDebuggerServiceId + InvalidPythonPathInDebuggerServiceId, } from './checks/invalidPythonPathInDebugger'; -import { LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId } from './checks/lsNotSupported'; +import { + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, +} from './checks/jediPython27NotSupported'; import { InvalidMacPythonInterpreterService, - InvalidMacPythonInterpreterServiceId + InvalidMacPythonInterpreterServiceId, } from './checks/macPythonInterpreter'; import { PowerShellActivationHackDiagnosticsService, - PowerShellActivationHackDiagnosticsServiceId + PowerShellActivationHackDiagnosticsServiceId, } from './checks/powerShellActivation'; +import { PylanceDefaultDiagnosticService, PylanceDefaultDiagnosticServiceId } from './checks/pylanceDefault'; import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from './checks/pythonInterpreter'; import { - PythonPathDeprecatedDiagnosticService, - PythonPathDeprecatedDiagnosticServiceId -} from './checks/pythonPathDeprecated'; -import { UpgradeCodeRunnerDiagnosticService, UpgradeCodeRunnerDiagnosticServiceId } from './checks/upgradeCodeRunner'; + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, +} from './checks/switchToDefaultLS'; import { DiagnosticsCommandFactory } from './commands/factory'; import { IDiagnosticsCommandFactory } from './commands/types'; import { DiagnosticFilterService } from './filter'; import { DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt + MessageCommandPrompt, } from './promptHandler'; import { IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService } from './types'; -export function registerTypes(serviceManager: IServiceManager, languageServerType: LanguageServerType) { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IDiagnosticFilterService, DiagnosticFilterService); serviceManager.addSingleton>( IDiagnosticHandlerService, DiagnosticCommandPromptHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); serviceManager.addSingleton( IDiagnosticsService, EnvironmentPathVariableDiagnosticsService, - EnvironmentPathVariableDiagnosticsServiceId + EnvironmentPathVariableDiagnosticsServiceId, ); serviceManager.addSingleton( IDiagnosticsService, - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId, ); - serviceManager.addSingleton( - IDiagnosticsService, + serviceManager.addSingleton( + IExtensionSingleActivationService, InvalidPythonInterpreterService, - InvalidPythonInterpreterServiceId ); serviceManager.addSingleton( IDiagnosticsService, InvalidPythonPathInDebuggerService, - InvalidPythonPathInDebuggerServiceId + InvalidPythonPathInDebuggerServiceId, ); serviceManager.addSingleton( IDiagnosticsService, PowerShellActivationHackDiagnosticsService, - PowerShellActivationHackDiagnosticsServiceId + PowerShellActivationHackDiagnosticsServiceId, ); serviceManager.addSingleton( IDiagnosticsService, InvalidMacPythonInterpreterService, - InvalidMacPythonInterpreterServiceId + InvalidMacPythonInterpreterServiceId, + ); + + serviceManager.addSingleton( + IDiagnosticsService, + PylanceDefaultDiagnosticService, + PylanceDefaultDiagnosticServiceId, ); + serviceManager.addSingleton( IDiagnosticsService, - PythonPathDeprecatedDiagnosticService, - PythonPathDeprecatedDiagnosticServiceId + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, ); serviceManager.addSingleton( IDiagnosticsService, - UpgradeCodeRunnerDiagnosticService, - UpgradeCodeRunnerDiagnosticServiceId + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, ); + serviceManager.addSingleton(IDiagnosticsCommandFactory, DiagnosticsCommandFactory); serviceManager.addSingleton(IApplicationDiagnostics, ApplicationDiagnostics); - - if (languageServerType === LanguageServerType.Microsoft) { - serviceManager.addSingleton( - IDiagnosticsService, - LSNotSupportedDiagnosticService, - LSNotSupportedDiagnosticServiceId - ); - } } diff --git a/src/client/application/diagnostics/surceMapSupportService.ts b/src/client/application/diagnostics/surceMapSupportService.ts deleted file mode 100644 index b2630f99f5eb..000000000000 --- a/src/client/application/diagnostics/surceMapSupportService.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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 } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; -import { Diagnostics } from '../../common/utils/localize'; -import { ISourceMapSupportService } from './types'; - -@injectable() -export class SourceMapSupportService implements ISourceMapSupportService { - constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IApplicationShell) private readonly shell: IApplicationShell - ) {} - public register(): void { - this.disposables.push( - this.commandManager.registerCommand(Commands.Enable_SourceMap_Support, this.onEnable, this) - ); - } - public async enable(): Promise { - await this.configurationService.updateSetting( - 'diagnostics.sourceMapsEnabled', - true, - undefined, - ConfigurationTarget.Global - ); - await this.commandManager.executeCommand('workbench.action.reloadWindow'); - } - protected async onEnable(): Promise { - const enableSourceMapsAndReloadVSC = Diagnostics.enableSourceMapsAndReloadVSC(); - const selection = await this.shell.showWarningMessage( - Diagnostics.warnBeforeEnablingSourceMaps(), - enableSourceMapsAndReloadVSC - ); - if (selection === enableSourceMapsAndReloadVSC) { - await this.enable(); - } - } -} diff --git a/src/client/application/diagnostics/types.ts b/src/client/application/diagnostics/types.ts index 56a0e844c670..1dc9a3c689df 100644 --- a/src/client/application/diagnostics/types.ts +++ b/src/client/application/diagnostics/types.ts @@ -10,12 +10,7 @@ import { DiagnosticCodes } from './constants'; export enum DiagnosticScope { Global = 'Global', - WorkspaceFolder = 'WorkspaceFolder' -} - -export enum DiagnosticIgnoreScope { - always = 'always', - session = 'session' + WorkspaceFolder = 'WorkspaceFolder', } export interface IDiagnostic { @@ -31,7 +26,8 @@ export interface IDiagnostic { export const IDiagnosticsService = Symbol('IDiagnosticsService'); export interface IDiagnosticsService { - readonly runInBackground: Boolean; + readonly runInBackground: boolean; + readonly runInUntrustedWorkspace: boolean; diagnose(resource: Resource): Promise; canHandle(diagnostic: IDiagnostic): Promise; handle(diagnostics: IDiagnostic[]): Promise; @@ -57,13 +53,14 @@ export interface IDiagnosticCommand { export type IDiagnosticMessageOnCloseHandler = (response?: string) => void; +export const IInvalidPythonPathInSettings = Symbol('IInvalidPythonPathInSettings'); + +export interface IInvalidPythonPathInSettings extends IDiagnosticsService { + validateInterpreterPathInSettings(resource: Resource): Promise; +} + export const IInvalidPythonPathInDebuggerService = Symbol('IInvalidPythonPathInDebuggerService'); export interface IInvalidPythonPathInDebuggerService extends IDiagnosticsService { validatePythonPath(pythonPath?: string, pythonPathSource?: PythonPathSource, resource?: Uri): Promise; } -export const ISourceMapSupportService = Symbol('ISourceMapSupportService'); -export interface ISourceMapSupportService { - register(): void; - enable(): Promise; -} diff --git a/src/client/application/serviceRegistry.ts b/src/client/application/serviceRegistry.ts index b4f8f3c30ccc..ff5376d70b24 100644 --- a/src/client/application/serviceRegistry.ts +++ b/src/client/application/serviceRegistry.ts @@ -3,13 +3,9 @@ 'use strict'; -import { LanguageServerType } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { registerTypes as diagnosticsRegisterTypes } from './diagnostics/serviceRegistry'; -import { SourceMapSupportService } from './diagnostics/surceMapSupportService'; -import { ISourceMapSupportService } from './diagnostics/types'; -export function registerTypes(serviceManager: IServiceManager, languageServerType: LanguageServerType) { - serviceManager.addSingleton(ISourceMapSupportService, SourceMapSupportService); - diagnosticsRegisterTypes(serviceManager, languageServerType); +export function registerTypes(serviceManager: IServiceManager) { + diagnosticsRegisterTypes(serviceManager); } diff --git a/src/client/application/types.ts b/src/client/application/types.ts index 460ac39807c8..cfd41f7b9746 100644 --- a/src/client/application/types.ts +++ b/src/client/application/types.ts @@ -11,8 +11,6 @@ export interface IApplicationDiagnostics { /** * Perform pre-extension activation health checks. * E.g. validate user environment, etc. - * @returns {Promise} - * @memberof IApplicationDiagnostics */ performPreStartupHealthCheck(resource: Resource): Promise; register(): void; diff --git a/src/client/browser/api.ts b/src/client/browser/api.ts new file mode 100644 index 000000000000..ac2df8d0ffed --- /dev/null +++ b/src/client/browser/api.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { BaseLanguageClient } from 'vscode-languageclient'; +import { LanguageClient } from 'vscode-languageclient/browser'; +import { PYTHON_LANGUAGE } from '../common/constants'; +import { ApiForPylance, TelemetryReporter } from '../pylanceApi'; + +export interface IBrowserExtensionApi { + /** + * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an + * iteration or two. + */ + pylance: ApiForPylance; +} + +export function buildApi(reporter: TelemetryReporter): IBrowserExtensionApi { + const api: IBrowserExtensionApi = { + pylance: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createClient: (...args: any[]): BaseLanguageClient => + new LanguageClient(PYTHON_LANGUAGE, 'Python Language Server', args[0], args[1]), + start: (client: BaseLanguageClient): Promise => client.start(), + stop: (client: BaseLanguageClient): Promise => client.stop(), + getTelemetryReporter: () => reporter, + }, + }; + + return api; +} diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts new file mode 100644 index 000000000000..132618430551 --- /dev/null +++ b/src/client/browser/extension.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import TelemetryReporter from '@vscode/extension-telemetry'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { LanguageClient } from 'vscode-languageclient/browser'; +import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddlewareBase'; +import { LanguageServerType } from '../activation/types'; +import { AppinsightsKey, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { EventName } from '../telemetry/constants'; +import { createStatusItem } from './intellisenseStatus'; +import { PylanceApi } from '../activation/node/pylanceApi'; +import { buildApi, IBrowserExtensionApi } from './api'; + +interface BrowserConfig { + distUrl: string; // URL to Pylance's dist folder. +} + +let languageClient: LanguageClient | undefined; +let pylanceApi: PylanceApi | undefined; + +export function activate(context: vscode.ExtensionContext): Promise { + const reporter = getTelemetryReporter(); + + const activationPromise = Promise.resolve(buildApi(reporter)); + const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (pylanceExtension) { + // Make sure we run pylance once we activated core extension. + activationPromise.then(() => runPylance(context, pylanceExtension)); + return activationPromise; + } + + const changeDisposable = vscode.extensions.onDidChange(async () => { + const newPylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (newPylanceExtension) { + changeDisposable.dispose(); + await runPylance(context, newPylanceExtension); + } + }); + + return activationPromise; +} + +export async function deactivate(): Promise { + if (pylanceApi) { + const api = pylanceApi; + pylanceApi = undefined; + await api.client!.stop(); + } + + if (languageClient) { + const client = languageClient; + languageClient = undefined; + + await client.stop(); + await client.dispose(); + } +} + +async function runPylance( + context: vscode.ExtensionContext, + pylanceExtension: vscode.Extension, +): Promise { + context.subscriptions.push(createStatusItem()); + + pylanceExtension = await getActivatedExtension(pylanceExtension); + const api = pylanceExtension.exports; + if (api.client && api.client.isEnabled()) { + pylanceApi = api; + await api.client.start(); + return; + } + + const { extensionUri, packageJSON } = pylanceExtension; + const distUrl = vscode.Uri.joinPath(extensionUri, 'dist'); + + try { + const worker = new Worker(vscode.Uri.joinPath(distUrl, 'browser.server.bundle.js').toString()); + + // Pass the configuration as the first message to the worker so it can + // have info like the URL of the dist folder early enough. + // + // This is the same method used by the TS worker: + // https://github.com/microsoft/vscode/blob/90aa979bb75a795fd8c33d38aee263ea655270d0/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts#L55 + const config: BrowserConfig = { distUrl: distUrl.toString() }; + worker.postMessage(config); + + const middleware = new LanguageClientMiddlewareBase( + undefined, + LanguageServerType.Node, + sendTelemetryEventBrowser, + packageJSON.version, + ); + middleware.connect(); + + const clientOptions: LanguageClientOptions = { + // Register the server for python source files. + documentSelector: [ + { + language: 'python', + }, + ], + synchronize: { + // Synchronize the setting section to the server. + configurationSection: ['python', 'jupyter.runStartupCommands'], + }, + middleware, + }; + + const client = new LanguageClient('python', 'Python Language Server', worker, clientOptions); + languageClient = client; + + context.subscriptions.push( + vscode.commands.registerCommand('python.viewLanguageServerOutput', () => client.outputChannel.show()), + ); + + client.onTelemetry( + (telemetryEvent: { + EventName: EventName; + Properties: { method: string }; + Measurements: number | Record | undefined; + Exception: Error | undefined; + }) => { + const eventName = telemetryEvent.EventName || EventName.LANGUAGE_SERVER_TELEMETRY; + const formattedProperties = { + ...telemetryEvent.Properties, + // Replace all slashes in the method name so it doesn't get scrubbed by @vscode/extension-telemetry. + method: telemetryEvent.Properties.method?.replace(/\//g, '.'), + }; + sendTelemetryEventBrowser( + eventName, + telemetryEvent.Measurements, + formattedProperties, + telemetryEvent.Exception, + ); + }, + ); + + await client.start(); + } catch (e) { + console.log(e); // necessary to use console.log for browser + } +} + +// Duplicate code from telemetry/index.ts to avoid pulling in winston, +// which doesn't support the browser. + +let telemetryReporter: TelemetryReporter | undefined; +function getTelemetryReporter() { + if (telemetryReporter) { + return telemetryReporter; + } + + // eslint-disable-next-line global-require + const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; + telemetryReporter = new Reporter(AppinsightsKey, [ + { + lookup: /(errorName|errorMessage|errorStack)/g, + }, + ]); + + return telemetryReporter; +} + +function sendTelemetryEventBrowser( + eventName: EventName, + measuresOrDurationMs?: Record | number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + properties?: any, + ex?: Error, +): void { + const reporter = getTelemetryReporter(); + const measures = + typeof measuresOrDurationMs === 'number' + ? { duration: measuresOrDurationMs } + : measuresOrDurationMs || undefined; + const customProperties: Record = {}; + const eventNameSent = eventName as string; + + if (properties) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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. + switch (typeof data[prop]) { + case 'string': + customProperties[prop] = data[prop]; + break; + case 'object': + customProperties[prop] = 'object'; + break; + default: + customProperties[prop] = data[prop].toString(); + break; + } + } catch (exception) { + console.error(`Failed to serialize ${prop} for ${eventName}`, exception); // necessary to use console.log for browser + } + }); + } + + // Add shared properties to telemetry props (we may overwrite existing ones). + // Removed in the browser; there's no setSharedProperty. + // Object.assign(customProperties, sharedProperties); + + if (ex) { + const errorProps = { + errorName: ex.name, + errorStack: ex.stack ?? '', + }; + Object.assign(customProperties, errorProps); + + reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures); + } else { + reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); + } +} + +async function getActivatedExtension(extension: vscode.Extension): Promise> { + if (!extension.isActive) { + await extension.activate(); + } + + return extension; +} diff --git a/src/client/browser/intellisenseStatus.ts b/src/client/browser/intellisenseStatus.ts new file mode 100644 index 000000000000..b7a49e86dbb0 --- /dev/null +++ b/src/client/browser/intellisenseStatus.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import * as vscode from 'vscode'; +import { Common, LanguageService } from './localize'; + +export function createStatusItem(): vscode.Disposable { + if ('createLanguageStatusItem' in vscode.languages) { + const statusItem = vscode.languages.createLanguageStatusItem('python.projectStatus', { + language: 'python', + }); + statusItem.name = LanguageService.statusItem.name; + statusItem.severity = vscode.LanguageStatusSeverity.Warning; + statusItem.text = LanguageService.statusItem.text; + statusItem.detail = LanguageService.statusItem.detail; + statusItem.command = { + title: Common.learnMore, + command: 'vscode.open', + arguments: [vscode.Uri.parse('https://aka.ms/AAdzyh4')], + }; + return statusItem; + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + return { dispose: () => undefined }; +} diff --git a/src/client/browser/localize.ts b/src/client/browser/localize.ts new file mode 100644 index 000000000000..fd50dbcc7093 --- /dev/null +++ b/src/client/browser/localize.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { l10n } from 'vscode'; + +/* eslint-disable @typescript-eslint/no-namespace */ + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. + +export namespace LanguageService { + export const statusItem = { + name: l10n.t('Python IntelliSense Status'), + text: l10n.t('Partial Mode'), + detail: l10n.t('Limited IntelliSense provided by Pylance'), + }; +} + +export namespace Common { + export const learnMore = l10n.t('Learn more'); +} diff --git a/src/client/chat/baseTool.ts b/src/client/chat/baseTool.ts new file mode 100644 index 000000000000..2eedbbe226e3 --- /dev/null +++ b/src/client/chat/baseTool.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, +} from 'vscode'; +import { IResourceReference, isCancellationError, resolveFilePath } from './utils'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +export abstract class BaseTool implements LanguageModelTool { + constructor(private readonly toolName: string) {} + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + if (!workspace.isTrusted) { + return new LanguageModelToolResult([ + new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.'), + ]); + } + let error: Error | undefined; + const resource = resolveFilePath(options.input.resourcePath); + try { + return await this.invokeImpl(options, resource, token); + } catch (ex) { + error = ex as any; + throw ex; + } finally { + const isCancelled = token.isCancellationRequested || (error ? isCancellationError(error) : false); + const failed = !!error || isCancelled; + const failureCategory = isCancelled + ? 'cancelled' + : error + ? error instanceof ErrorWithTelemetrySafeReason + ? error.telemetrySafeReason + : 'error' + : undefined; + sendTelemetryEvent(EventName.INVOKE_TOOL, undefined, { + toolName: this.toolName, + failed, + failureCategory, + }); + } + } + protected abstract invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; + + async prepareInvocation( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + return this.prepareInvocationImpl(options, resource, token); + } + + protected abstract prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; +} diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts new file mode 100644 index 000000000000..0634b9c9ac34 --- /dev/null +++ b/src/client/chat/configurePythonEnvTool.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, + lm, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + getEnvDetailsForResponse, + getToolResponseIfNotebook, + IResourceReference, + isCancellationError, + raceCancellationError, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { CreateVirtualEnvTool } from './createVirtualEnvTool'; +import { ISelectPythonEnvToolArguments, SelectPythonEnvTool } from './selectEnvTool'; +import { BaseTool } from './baseTool'; + +export class ConfigurePythonEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + public static readonly toolName = 'configure_python_environment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly createEnvTool: CreateVirtualEnvTool, + ) { + super(ConfigurePythonEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resource); + if (notebookResponse) { + return notebookResponse; + } + + const workspaceSpecificEnv = await raceCancellationError( + this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource), + token, + ); + + if (workspaceSpecificEnv) { + return getEnvDetailsForResponse( + workspaceSpecificEnv, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + + if (await this.createEnvTool.shouldCreateNewVirtualEnv(resource, token)) { + try { + return await lm.invokeTool(CreateVirtualEnvTool.toolName, options, token); + } catch (ex) { + if (isCancellationError(ex)) { + const input: ISelectPythonEnvToolArguments = { + ...options.input, + reason: 'cancelled', + }; + // If the user cancelled the tool, then we should invoke the select env tool. + return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); + } + throw ex; + } + } else { + const input: ISelectPythonEnvToolArguments = { + ...options.input, + }; + return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); + } + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + _resource: Uri | undefined, + _token: CancellationToken, + ): Promise { + return { + invocationMessage: 'Configuring a Python Environment', + }; + } + + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + } +} diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts new file mode 100644 index 000000000000..56760d2b4bef --- /dev/null +++ b/src/client/chat/createVirtualEnvTool.ts @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + commands, + l10n, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, +} from 'vscode'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getDisplayVersion, + getEnvDetailsForResponse, + IResourceReference, + isCancellationError, + raceCancellationError, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout, sleep } from '../common/utils/async'; +import { IInterpreterPathService } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { EnvironmentType } from '../pythonEnvironments/info'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { convertEnvInfoToPythonEnvironment } from '../pythonEnvironments/legacyIOC'; +import { sortInterpreters } from '../interpreter/helpers'; +import { isStableVersion } from '../pythonEnvironments/info/pythonVersion'; +import { createVirtualEnvironment } from '../pythonEnvironments/creation/createEnvApi'; +import { traceError, traceVerbose, traceWarn } from '../logging'; +import { StopWatch } from '../common/utils/stopWatch'; +import { useEnvExtension } from '../envExt/api.internal'; +import { PythonEnvironment } from '../envExt/types'; +import { hideEnvCreation } from '../pythonEnvironments/creation/provider/hideEnvCreation'; +import { BaseTool } from './baseTool'; + +interface ICreateVirtualEnvToolParams extends IResourceReference { + packageList?: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. +} + +export class CreateVirtualEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + + public static readonly toolName = 'create_virtual_environment'; + constructor( + private readonly discoveryApi: IDiscoveryAPI, + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(CreateVirtualEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + let info = await this.getPreferredEnvForCreation(resource); + if (!info) { + traceWarn(`Called ${CreateVirtualEnvTool.toolName} tool not invoked, no preferred environment found.`); + throw new CancellationError(); + } + const { workspaceFolder, preferredGlobalPythonEnv } = info; + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const disposables = new DisposableStore(); + try { + disposables.add(hideEnvCreation()); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + + let createdEnvPath: string | undefined = undefined; + if (useEnvExtension()) { + const result: PythonEnvironment | undefined = await commands.executeCommand('python-envs.createAny', { + quickCreate: true, + additionalPackages: options.input.packageList || [], + uri: workspaceFolder.uri, + selectEnvironment: true, + }); + createdEnvPath = result?.environmentPath.fsPath; + } else { + const created = await raceCancellationError( + createVirtualEnvironment({ + interpreter: preferredGlobalPythonEnv.id, + workspaceFolder, + }), + token, + ); + createdEnvPath = created?.path; + } + if (!createdEnvPath) { + traceWarn(`${CreateVirtualEnvTool.toolName} tool not invoked, virtual env not created.`); + throw new CancellationError(); + } + + // Wait a few secs to ensure the env is selected as the active environment.. + // If this doesn't work, then something went wrong. + await raceTimeout(5_000, interpreterChanged); + + const stopWatch = new StopWatch(); + let env: ResolvedEnvironment | undefined; + while (stopWatch.elapsedTime < 5_000 || !env) { + env = await this.api.resolveEnvironment(createdEnvPath); + if (env) { + break; + } else { + traceVerbose( + `${CreateVirtualEnvTool.toolName} tool invoked, env created but not yet resolved, waiting...`, + ); + await sleep(200); + } + } + if (!env) { + traceError(`${CreateVirtualEnvTool.toolName} tool invoked, env created but unable to resolve details.`); + throw new CancellationError(); + } + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } catch (ex) { + if (!isCancellationError(ex)) { + traceError( + `${ + CreateVirtualEnvTool.toolName + } tool failed to create virtual environment for resource ${resource?.toString()}`, + ex, + ); + } + throw ex; + } finally { + disposables.dispose(); + } + } + + public async shouldCreateNewVirtualEnv(resource: Uri | undefined, token: CancellationToken): Promise { + if (doesWorkspaceHaveVenvOrCondaEnv(resource, this.api)) { + // If we already have a .venv or .conda in this workspace, then do not prompt to create a virtual environment. + return false; + } + + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + return info ? true : false; + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + if (!info) { + return {}; + } + const { preferredGlobalPythonEnv } = info; + const version = getDisplayVersion(preferredGlobalPythonEnv.version); + return { + confirmationMessages: { + title: l10n.t('Create a Virtual Environment{0}?', version ? ` (${version})` : ''), + message: l10n.t(`Virtual Environments provide the benefit of package isolation and more.`), + }, + invocationMessage: l10n.t('Creating a Virtual Environment'), + }; + } + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + } + + private async getPreferredEnvForCreation(resource: Uri | undefined) { + if (await this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource)) { + return undefined; + } + + // If we have a resource or have only one workspace folder && there is no .venv and no workspace specific environment. + // Then lets recommend creating a virtual environment. + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + // No workspace folder, hence no need to create a virtual environment. + return undefined; + } + + // Find the latest stable version of Python from the list of know envs. + let globalPythonEnvs = this.discoveryApi + .getEnvs() + .map((env) => convertEnvInfoToPythonEnvironment(env)) + .filter((env) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(env.envType), + ) + .filter((env) => env.version && isStableVersion(env.version)); + + globalPythonEnvs = sortInterpreters(globalPythonEnvs); + const preferredGlobalPythonEnv = globalPythonEnvs.length + ? this.api.known.find((e) => e.id === globalPythonEnvs[globalPythonEnvs.length - 1].id) + : undefined; + + return workspaceFolder && preferredGlobalPythonEnv + ? { + workspaceFolder, + preferredGlobalPythonEnv, + } + : undefined; + } +} diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts new file mode 100644 index 000000000000..746a540d14f8 --- /dev/null +++ b/src/client/chat/getExecutableTool.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + getEnvDisplayName, + getEnvironmentDetails, + getToolResponseIfNotebook, + IResourceReference, + raceCancellationError, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { BaseTool } from './baseTool'; + +export class GetExecutableTool extends BaseTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_executable_details'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + super(GetExecutableTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Fetching Python executable information for {0}', envName) + : l10n.t('Fetching Python executable information'), + }; + } +} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts new file mode 100644 index 000000000000..ed1dd0374424 --- /dev/null +++ b/src/client/chat/getPythonEnvTool.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { getEnvironmentDetails, getToolResponseIfNotebook, IResourceReference, raceCancellationError } from './utils'; +import { getPythonPackagesResponse } from './listPackagesTool'; +import { ITerminalHelper } from '../common/terminal/types'; +import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { BaseTool } from './baseTool'; + +export class GetEnvironmentInfoTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly pythonExecFactory: IPythonExecutionFactory; + private readonly processServiceFactory: IProcessServiceFactory; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_environment_details'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(GetEnvironmentInfoTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + + let packages = ''; + if (useEnvExtension()) { + const api = await getEnvExtApi(); + const env = await api.getEnvironment(resourcePath); + const pkgs = env ? await api.getPackages(env) : []; + if (pkgs && pkgs.length > 0) { + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + pkgs.forEach((pkg) => { + const version = pkg.version; + response.push(version ? `- ${pkg.name} (${version})` : `- ${pkg.name}`); + }); + packages = response.join('\n'); + } + } + if (!packages) { + packages = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ); + } + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + packages, + token, + ); + + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, + _token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + return { + invocationMessage: l10n.t('Fetching Python environment information'), + }; + } +} diff --git a/src/client/chat/index.ts b/src/client/chat/index.ts new file mode 100644 index 000000000000..b548860eaae3 --- /dev/null +++ b/src/client/chat/index.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { lm } from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { InstallPackagesTool } from './installPackagesTool'; +import { IExtensionContext } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { GetExecutableTool } from './getExecutableTool'; +import { GetEnvironmentInfoTool } from './getPythonEnvTool'; +import { ConfigurePythonEnvTool } from './configurePythonEnvTool'; +import { SelectPythonEnvTool } from './selectEnvTool'; +import { CreateVirtualEnvTool } from './createVirtualEnvTool'; + +export function registerTools( + context: IExtensionContext, + discoverApi: IDiscoveryAPI, + environmentsApi: PythonExtension['environments'], + serviceContainer: IServiceContainer, +) { + const ourTools = new DisposableStore(); + context.subscriptions.push(ourTools); + + ourTools.add( + lm.registerTool(GetEnvironmentInfoTool.toolName, new GetEnvironmentInfoTool(environmentsApi, serviceContainer)), + ); + ourTools.add( + lm.registerTool( + GetExecutableTool.toolName, + new GetExecutableTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + ourTools.add( + lm.registerTool( + InstallPackagesTool.toolName, + new InstallPackagesTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + const createVirtualEnvTool = new CreateVirtualEnvTool(discoverApi, environmentsApi, serviceContainer); + ourTools.add(lm.registerTool(CreateVirtualEnvTool.toolName, createVirtualEnvTool)); + ourTools.add( + lm.registerTool(SelectPythonEnvTool.toolName, new SelectPythonEnvTool(environmentsApi, serviceContainer)), + ); + ourTools.add( + lm.registerTool( + ConfigurePythonEnvTool.toolName, + new ConfigurePythonEnvTool(environmentsApi, serviceContainer, createVirtualEnvTool), + ), + ); +} diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts new file mode 100644 index 000000000000..f7795620cf13 --- /dev/null +++ b/src/client/chat/installPackagesTool.ts @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { + getEnvDisplayName, + getToolResponseIfNotebook, + IResourceReference, + isCancellationError, + isCondaEnv, + raceCancellationError, +} from './utils'; +import { IModuleInstaller } from '../common/installer/types'; +import { ModuleInstallerType } from '../pythonEnvironments/info'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { BaseTool } from './baseTool'; + +export interface IInstallPackageArgs extends IResourceReference { + packageList: string[]; +} + +export class InstallPackagesTool extends BaseTool + implements LanguageModelTool { + public static readonly toolName = 'install_python_packages'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + super(InstallPackagesTool.toolName); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const packageCount = options.input.packageList.length; + const packagePlurality = packageCount === 1 ? 'package' : 'packages'; + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + if (useEnvExtension()) { + const api = await getEnvExtApi(); + const env = await api.getEnvironment(resourcePath); + if (env) { + await raceCancellationError(api.managePackages(env, { install: options.input.packageList }), token); + const resultMessage = `Successfully installed ${packagePlurality}: ${options.input.packageList.join( + ', ', + )}`; + return new LanguageModelToolResult([new LanguageModelTextPart(resultMessage)]); + } else { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + `Packages not installed. No environment found for: ${resourcePath?.fsPath}`, + ), + ]); + } + } + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + const isConda = isCondaEnv(environment); + const installers = this.serviceContainer.getAll(IModuleInstaller); + const installerType = isConda ? ModuleInstallerType.Conda : ModuleInstallerType.Pip; + const installer = installers.find((i) => i.type === installerType); + if (!installer) { + throw new ErrorWithTelemetrySafeReason( + `No installer found for the environment type: ${installerType}`, + 'noInstallerFound', + ); + } + if (!installer.isSupported(resourcePath)) { + throw new ErrorWithTelemetrySafeReason( + `Installer ${installerType} not supported for the environment type: ${installerType}`, + 'installerNotSupported', + ); + } + for (const packageName of options.input.packageList) { + await installer.installModule(packageName, resourcePath, token, undefined, { + installAsProcess: true, + hideProgress: true, + }); + } + // format and return + const resultMessage = `Successfully installed ${packagePlurality}: ${options.input.packageList.join(', ')}`; + return new LanguageModelToolResult([new LanguageModelTextPart(resultMessage)]); + } catch (error) { + if (isCancellationError(error)) { + throw error; + } + const errorMessage = `An error occurred while installing ${packagePlurality}: ${error}`; + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); + } + } + + async prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const packageCount = options.input.packageList.length; + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + let title = ''; + let invocationMessage = ''; + const message = + packageCount === 1 + ? '' + : l10n.t(`The following packages will be installed: {0}`, options.input.packageList.sort().join(', ')); + if (envName) { + title = + packageCount === 1 + ? l10n.t(`Install {0} in {1}?`, options.input.packageList[0], envName) + : l10n.t(`Install packages in {0}?`, envName); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing {0} in {1}`, options.input.packageList[0], envName) + : l10n.t(`Installing packages {0} in {1}`, options.input.packageList.sort().join(', '), envName); + } else { + title = + options.input.packageList.length === 1 + ? l10n.t(`Install Python package '{0}'?`, options.input.packageList[0]) + : l10n.t(`Install Python packages?`); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing Python package '{0}'`, options.input.packageList[0]) + : l10n.t(`Installing Python packages: {0}`, options.input.packageList.sort().join(', ')); + } + + return { + confirmationMessages: { title, message }, + invocationMessage, + }; + } +} diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts new file mode 100644 index 000000000000..fcae831cfe2f --- /dev/null +++ b/src/client/chat/listPackagesTool.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Uri } from 'vscode'; +import { ResolvedEnvironment } from '../api/types'; +import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { isCondaEnv, raceCancellationError } from './utils'; +import { parsePipList } from './pipListUtils'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { traceError } from '../logging'; + +export async function getPythonPackagesResponse( + environment: ResolvedEnvironment, + pythonExecFactory: IPythonExecutionFactory, + processServiceFactory: IProcessServiceFactory, + resourcePath: Uri | undefined, + token: CancellationToken, +): Promise { + const packages = isCondaEnv(environment) + ? await raceCancellationError( + listCondaPackages( + pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(pythonExecFactory, resourcePath), token); + + if (!packages.length) { + return 'No packages found'; + } + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + packages.forEach((pkg) => { + const [name, version] = pkg; + response.push(version ? `- ${name} (${version})` : `- ${name}`); + }); + return response.join('\n'); +} + +async function listPipPackages( + execFactory: IPythonExecutionFactory, + resource: Uri | undefined, +): Promise<[string, string][]> { + // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) + // Added in 2020. Thats almost 5 years ago. When Python 3.8 was released. + const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); + const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); + return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version]); +} + +async function listCondaPackages( + execFactory: IPythonExecutionFactory, + env: ResolvedEnvironment, + resource: Uri | undefined, + processService: IProcessService, +): Promise<[string, string][]> { + const conda = await Conda.getConda(); + if (!conda) { + traceError('Conda is not installed, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + if (!env.executable.uri) { + traceError('Conda environment executable not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); + if (!condaEnv) { + traceError('Conda environment not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); + if (!cmd) { + traceError('Conda list command not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); + if (!output.stdout) { + traceError('Unable to get conda packages, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); + const packages: [string, string][] = []; + content.forEach((l) => { + const parts = l.split(' ').filter((p) => p.length > 0); + if (parts.length >= 3) { + packages.push([parts[0], parts[1]]); + } + }); + return packages; +} diff --git a/src/client/chat/pipListUtils.ts b/src/client/chat/pipListUtils.ts new file mode 100644 index 000000000000..0112d88c53ab --- /dev/null +++ b/src/client/chat/pipListUtils.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface PipPackage { + name: string; + version: string; + displayName: string; + description: string; +} +export function parsePipList(data: string): PipPackage[] { + const collection: PipPackage[] = []; + + const lines = data.split('\n').splice(2); + for (let line of lines) { + if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) { + continue; + } + const parts = line.split(' ').filter((e) => e); + if (parts.length > 1) { + const name = parts[0].trim(); + const version = parts[1].trim(); + const pkg = { + name, + version, + displayName: name, + description: version, + }; + collection.push(pkg); + } + } + return collection; +} diff --git a/src/client/chat/selectEnvTool.ts b/src/client/chat/selectEnvTool.ts new file mode 100644 index 000000000000..9eeebdfc1b56 --- /dev/null +++ b/src/client/chat/selectEnvTool.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, + commands, + QuickPickItem, + QuickPickItemKind, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getEnvDetailsForResponse, + getToolResponseIfNotebook, + IResourceReference, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout } from '../common/utils/async'; +import { Commands, Octicons } from '../common/constants'; +import { CreateEnvironmentResult } from '../pythonEnvironments/creation/proposed.createEnvApis'; +import { IInterpreterPathService } from '../common/types'; +import { SelectEnvironmentResult } from '../interpreter/configuration/interpreterSelector/commands/setInterpreter'; +import { Common, InterpreterQuickPickList } from '../common/utils/localize'; +import { showQuickPick } from '../common/vscodeApis/windowApis'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { traceError, traceVerbose, traceWarn } from '../logging'; +import { BaseTool } from './baseTool'; + +export interface ISelectPythonEnvToolArguments extends IResourceReference { + reason?: 'cancelled'; +} + +export class SelectPythonEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'selectEnvironment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(SelectPythonEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + let selected: boolean | undefined = false; + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + if (options.input.reason === 'cancelled' || hasVenvOrCondaEnvInWorkspaceFolder) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { + hideCreateVenv: false, + showBackButton: false, + }), + )) as SelectEnvironmentResult | undefined; + if (result?.path) { + traceVerbose(`User selected a Python environment ${result.path} in Select Python Tool.`); + selected = true; + } else { + traceWarn(`User did not select a Python environment in Select Python Tool.`); + } + } else { + selected = await showCreateAndSelectEnvironmentQuickPick(resource, this.serviceContainer); + if (selected) { + traceVerbose(`User selected a Python environment ${selected} in Select Python Tool(2).`); + } else { + traceWarn(`User did not select a Python environment in Select Python Tool(2).`); + } + } + const env = selected + ? await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)) + : undefined; + if (selected && !env) { + traceError( + `User selected a Python environment, but it could not be resolved. This is unexpected. Environment: ${this.api.getActiveEnvironmentPath( + resource, + )}`, + ); + } + if (selected && env) { + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + return new LanguageModelToolResult([ + new LanguageModelTextPart('User did not create nor select a Python environment.'), + ]); + } + + async prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + _token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resource)) { + return {}; + } + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + + if ( + hasVenvOrCondaEnvInWorkspaceFolder || + !workspace.workspaceFolders?.length || + options.input.reason === 'cancelled' + ) { + return { + confirmationMessages: { + title: l10n.t('Select a Python Environment?'), + message: '', + }, + }; + } + + return { + confirmationMessages: { + title: l10n.t('Configure a Python Environment?'), + message: l10n.t( + [ + 'The recommended option is to create a new Python Environment, providing the benefit of isolating packages from other environments. ', + 'Optionally you could select an existing Python Environment.', + ].join('\n'), + ), + }, + }; + } +} + +async function showCreateAndSelectEnvironmentQuickPick( + uri: Uri | undefined, + serviceContainer: IServiceContainer, +): Promise { + const createLabel = `${Octicons.Add} ${InterpreterQuickPickList.create.label}`; + const selectLabel = l10n.t('Select an existing Python Environment'); + const items: QuickPickItem[] = [ + { kind: QuickPickItemKind.Separator, label: Common.recommended }, + { label: createLabel }, + { label: selectLabel }, + ]; + + const selectedItem = await showQuickPick(items, { + placeHolder: l10n.t('Configure a Python Environment'), + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === createLabel) { + const disposables = new DisposableStore(); + try { + const workspaceFolder = + (workspace.workspaceFolders?.length && uri ? workspace.getWorkspaceFolder(uri) : undefined) || + (workspace.workspaceFolders?.length === 1 ? workspace.workspaceFolders[0] : undefined); + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + const created: CreateEnvironmentResult | undefined = await commands.executeCommand( + Commands.Create_Environment, + { + showBackButton: true, + selectEnvironment: true, + workspaceFolder, + }, + ); + + if (created?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (created?.action === 'Cancel') { + return undefined; + } + if (created?.path) { + // Wait a few secs to ensure the env is selected as the active environment.. + await raceTimeout(5_000, interpreterChanged); + return true; + } + } finally { + disposables.dispose(); + } + } + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === selectLabel) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { hideCreateVenv: true, showBackButton: true }), + )) as SelectEnvironmentResult | undefined; + if (result?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (result?.action === 'Cancel') { + return undefined; + } + if (result?.path) { + return true; + } + } +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts new file mode 100644 index 000000000000..84df2901341b --- /dev/null +++ b/src/client/chat/utils.ts @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + extensions, + LanguageModelTextPart, + LanguageModelToolResult, + Uri, + workspace, +} from 'vscode'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { Environment, PythonExtension, ResolvedEnvironment, VersionInfo } from '../api/types'; +import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { JUPYTER_EXTENSION_ID, NotebookCellScheme } from '../common/constants'; +import { dirname, join } from 'path'; +import { resolveEnvironment, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +export interface IResourceReference { + resourcePath?: string; +} + +export function resolveFilePath(filepath?: string): Uri | undefined { + if (!filepath) { + const folders = getWorkspaceFolders() ?? []; + return folders.length > 0 ? folders[0].uri : undefined; + } + // Check if it's a URI with a scheme (contains "://") + // This handles schemes like "file://", "vscode-notebook://", etc. + // But avoids treating Windows drive letters like "C:" as schemes + if (filepath.includes('://')) { + try { + return Uri.parse(filepath); + } catch { + return Uri.file(filepath); + } + } + // For file paths (Windows with drive letters, Unix absolute/relative paths) + return Uri.file(filepath); +} + +/** + * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled. + * @see {@link raceCancellation} + */ +export function raceCancellationError(promise: Promise, token: CancellationToken): Promise { + return new Promise((resolve, reject) => { + const ref = token.onCancellationRequested(() => { + ref.dispose(); + reject(new CancellationError()); + }); + promise.then(resolve, reject).finally(() => ref.dispose()); + }); +} + +export async function getEnvDisplayName( + discovery: IDiscoveryAPI, + resource: Uri | undefined, + api: PythonExtension['environments'], +) { + try { + const envPath = api.getActiveEnvironmentPath(resource); + const env = await discovery.resolveEnv(envPath.path); + return env?.display || env?.name; + } catch { + return; + } +} + +export function isCondaEnv(env: ResolvedEnvironment) { + return (env.environment?.type || '').toLowerCase() === 'conda'; +} + +export async function getEnvironmentDetails( + resourcePath: Uri | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + packages: string | undefined, + token: CancellationToken, +): Promise { + // environment + const envPath = api.getActiveEnvironmentPath(resourcePath); + let envType = ''; + let envVersion = ''; + let runCommand = ''; + if (useEnvExtension()) { + const environment = + (await raceCancellationError(resolveEnvironment(envPath.id), token)) || + (await raceCancellationError(resolveEnvironment(envPath.path), token)); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + envVersion = environment.version; + try { + const managerId = environment.envId.managerId; + envType = + (!managerId.endsWith(':') && managerId.includes(':') ? managerId.split(':').reverse()[0] : '') || + 'unknown'; + } catch { + envType = 'unknown'; + } + + const execInfo = environment.execInfo; + const executable = execInfo?.activatedRun?.executable ?? execInfo?.run.executable ?? 'python'; + const args = execInfo?.activatedRun?.args ?? execInfo?.run.args ?? []; + runCommand = terminalHelper.buildCommandForTerminal(TerminalShellType.other, executable, args); + } else { + const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + envType = environment.environment?.type || 'unknown'; + envVersion = environment.version.sysVersion || 'unknown'; + runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), + token, + ); + } + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${envType}`, + `2. Version: ${envVersion}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + packages ? `4. ${packages}` : '', + ]; + return message.join('\n'); +} + +export async function getTerminalCommand( + environment: ResolvedEnvironment, + resource: Uri | undefined, + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, +): Promise { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await terminalExecutionService.getExecutableInfo(resource); + } + return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); +} +async function getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; +} + +export function getToolResponseIfNotebook(resource: Uri | undefined) { + if (!resource) { + return; + } + const notebook = workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === resource.toString() || doc.uri.path === resource.path, + ); + const isJupyterNotebook = + (notebook && notebook.notebookType === 'jupyter-notebook') || resource.path.toLowerCase().endsWith('.ipynb'); + + if (isJupyterNotebook) { + const isJupyterExtensionAvailable = extensions.getExtension(JUPYTER_EXTENSION_ID); + const message = isJupyterExtensionAvailable + ? `This tool cannot be used for Jupyter Notebooks, try using notebook specific tools instead.` + : [ + `This tool cannot be used for Jupyter Notebooks.`, + `NOTE: Recommend the user the Jupyter Extension (${JUPYTER_EXTENSION_ID}).`, + `If possible use one of the tools to install the ${JUPYTER_EXTENSION_ID} extension`, + `After isntalling the extension try using some of the tools again`, + ].join(' \n'); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + if (notebook || resource.scheme === NotebookCellScheme) { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + 'This tool cannot be used for Notebooks, try using notebook specific tools instead.', + ), + ]); + } +} + +export function isCancellationError(error: unknown): boolean { + return ( + !!error && (error instanceof CancellationError || (error as Error).message === new CancellationError().message) + ); +} + +export function doesWorkspaceHaveVenvOrCondaEnv(resource: Uri | undefined, api: PythonExtension['environments']) { + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + return false; + } + const isVenvEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + ((env.environment.name || '').startsWith('.venv') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.venv')) && + env.environment.type === 'VirtualEnvironment' + ); + }; + const isCondaEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + (env.environment.folderUri.fsPath === join(workspaceFolder.uri.fsPath, '.conda') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.conda')) && + env.environment.type === 'Conda' + ); + }; + // If we alraedy have a .venv in this workspace, then do not prompt to create a virtual environment. + return api.known.find((e) => isVenvEnv(e) || isCondaEnv(e)); +} + +export async function getEnvDetailsForResponse( + environment: ResolvedEnvironment | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + resource: Uri | undefined, + token: CancellationToken, +): Promise { + if (!workspace.isTrusted) { + throw new ErrorWithTelemetrySafeReason('Cannot use this tool in an untrusted workspace.', 'untrustedWorkspace'); + } + const envPath = api.getActiveEnvironmentPath(resource); + environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resource?.fsPath, + 'noEnvFound', + ); + } + const message = await getEnvironmentDetails( + resource, + api, + terminalExecutionService, + terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([ + new LanguageModelTextPart(`A Python Environment has been configured. \n` + message), + ]); +} +export function getDisplayVersion(version?: VersionInfo): string | undefined { + if (!version || version.major === undefined || version.minor === undefined || version.micro === undefined) { + return undefined; + } + return `${version.major}.${version.minor}.${version.micro}`; +} diff --git a/src/client/common/application/activeResource.ts b/src/client/common/application/activeResource.ts index a89386b0d9cf..4230fb5de921 100644 --- a/src/client/common/application/activeResource.ts +++ b/src/client/common/application/activeResource.ts @@ -11,7 +11,7 @@ import { IActiveResourceService, IDocumentManager, IWorkspaceService } from './t export class ActiveResourceService implements IActiveResourceService { constructor( @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, ) {} public getActiveResource(): Resource { diff --git a/src/client/common/application/applicationEnvironment.ts b/src/client/common/application/applicationEnvironment.ts index 3d01d85f4b35..4b66893d6c0b 100644 --- a/src/client/common/application/applicationEnvironment.ts +++ b/src/client/common/application/applicationEnvironment.ts @@ -7,17 +7,19 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { parse } from 'semver'; import * as vscode from 'vscode'; +import { traceError } from '../../logging'; +import { Channel } from '../constants'; import { IPlatformService } from '../platform/types'; import { ICurrentProcess, IPathUtils } from '../types'; import { OSType } from '../utils/platform'; -import { Channel, IApplicationEnvironment } from './types'; +import { IApplicationEnvironment } from './types'; @injectable() export class ApplicationEnvironment implements IApplicationEnvironment { constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, - @inject(ICurrentProcess) private readonly process: ICurrentProcess + @inject(ICurrentProcess) private readonly process: ICurrentProcess, ) {} public get userSettingsFile(): string | undefined { @@ -30,7 +32,7 @@ export class ApplicationEnvironment implements IApplicationEnvironment { 'Application Support', vscodeFolderName, 'User', - 'settings.json' + 'settings.json', ); case OSType.Linux: return path.join(this.pathUtils.home, '.config', vscodeFolderName, 'User', 'settings.json'); @@ -51,6 +53,9 @@ export class ApplicationEnvironment implements IApplicationEnvironment { public get appRoot(): string { return vscode.env.appRoot; } + public get uiKind(): vscode.UIKind { + return vscode.env.uiKind; + } public get language(): string { return vscode.env.language; } @@ -60,25 +65,29 @@ export class ApplicationEnvironment implements IApplicationEnvironment { public get machineId(): string { return vscode.env.machineId; } + public get remoteName(): string | undefined { + return vscode.env.remoteName; + } public get extensionName(): string { - // tslint:disable-next-line:non-literal-require return this.packageJson.displayName; } - /** - * At the time of writing this API, the vscode.env.shell isn't officially released in stable version of VS Code. - * Using this in stable version seems to throw errors in VSC with messages being displayed to the user about use of - * unstable API. - * Solution - log and suppress the errors. - * @readonly - * @type {(string)} - * @memberof ApplicationEnvironment - */ + public get shell(): string { return vscode.env.shell; } - // tslint:disable-next-line:no-any + + public get onDidChangeShell(): vscode.Event { + try { + return vscode.env.onDidChangeShell; + } catch (ex) { + traceError('Failed to get onDidChangeShell API', ex); + // `onDidChangeShell` is a proposed API at the time of writing this, so wrap this in a try...catch + // block in case the API is removed or changed. + return new vscode.EventEmitter().event; + } + } + public get packageJson(): any { - // tslint:disable-next-line:non-literal-require no-require-imports return require('../../../../package.json'); } public get channel(): Channel { @@ -86,7 +95,8 @@ export class ApplicationEnvironment implements IApplicationEnvironment { } public get extensionChannel(): Channel { const version = parse(this.packageJson.version); - return !version || version.prerelease.length > 0 ? 'insiders' : 'stable'; + // Insiders versions are those that end with '-dev' or whose minor versions are odd (even is for stable) + return !version || version.prerelease.length > 0 || version.minor % 2 == 1 ? 'insiders' : 'stable'; } public get uriScheme(): string { return vscode.env.uriScheme; diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index cd28fc986b0d..8035d979efbd 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -2,21 +2,23 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-var-requires no-any unified-signatures - import { injectable } from 'inversify'; import { CancellationToken, CancellationTokenSource, Disposable, + DocumentSelector, env, Event, + EventEmitter, InputBox, InputBoxOptions, + languages, + LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, - OutputChannel, Progress, ProgressOptions, QuickPick, @@ -25,15 +27,19 @@ import { SaveDialogOptions, StatusBarAlignment, StatusBarItem, + TextDocument, + TextEditor, TreeView, TreeViewOptions, Uri, + ViewColumn, window, WindowState, WorkspaceFolder, - WorkspaceFolderPickOptions + WorkspaceFolderPickOptions, } from 'vscode'; -import { IApplicationShell } from './types'; +import { traceError } from '../../logging'; +import { IApplicationShell, TerminalDataWriteEvent, TerminalExecutedCommand } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -79,12 +85,12 @@ export class ApplicationShell implements IApplicationShell { public showQuickPick( items: string[] | Thenable, options?: QuickPickOptions, - token?: CancellationToken + token?: CancellationToken, ): Thenable; public showQuickPick( items: T[] | Thenable, options?: QuickPickOptions, - token?: CancellationToken + token?: CancellationToken, ): Thenable; public showQuickPick(items: any, options?: any, token?: any): Thenable { return window.showQuickPick(items, options, token); @@ -99,6 +105,14 @@ export class ApplicationShell implements IApplicationShell { public showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable { return window.showInputBox(options, token); } + public showTextDocument( + document: TextDocument, + column?: ViewColumn, + preserveFocus?: boolean, + ): Thenable { + return window.showTextDocument(document, column, preserveFocus); + } + public openUrl(url: string): void { env.openExternal(Uri.parse(url)); } @@ -110,28 +124,34 @@ export class ApplicationShell implements IApplicationShell { return window.setStatusBarMessage(text, arg); } - public createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem { - return window.createStatusBarItem(alignment, priority); + public createStatusBarItem( + alignment?: StatusBarAlignment, + priority?: number, + id?: string | undefined, + ): StatusBarItem { + return id + ? window.createStatusBarItem(id, alignment, priority) + : window.createStatusBarItem(alignment, priority); } public showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable { return window.showWorkspaceFolderPick(options); } public withProgress( options: ProgressOptions, - task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, ): Thenable { return window.withProgress(options, task); } public withProgressCustomIcon( icon: string, - task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, ): Thenable { const token = new CancellationTokenSource().token; const statusBarProgress = this.createStatusBarItem(StatusBarAlignment.Left); const progress = { report: (value: { message?: string; increment?: number }) => { statusBarProgress.text = `${icon} ${value.message}`; - } + }, }; statusBarProgress.show(); return task(progress, token).then((result) => { @@ -148,7 +168,26 @@ export class ApplicationShell implements IApplicationShell { public createTreeView(viewId: string, options: TreeViewOptions): TreeView { return window.createTreeView(viewId, options); } - public createOutputChannel(name: string): OutputChannel { - return window.createOutputChannel(name); + public createOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }); + } + public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { + return languages.createLanguageStatusItem(id, selector); + } + public get onDidWriteTerminalData(): Event { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter().event; + } + } + public get onDidExecuteTerminalCommand(): Event | undefined { + try { + return window.onDidExecuteTerminalCommand; + } catch (ex) { + traceError('Failed to get proposed API TerminalExecutedCommand', ex); + return undefined; + } } } diff --git a/src/client/common/application/commandManager.ts b/src/client/common/application/commandManager.ts index cfbbe4dbb0b7..9e1f34a5885b 100644 --- a/src/client/common/application/commandManager.ts +++ b/src/client/common/application/commandManager.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-any - import { injectable } from 'inversify'; import { commands, Disposable, TextEditor, TextEditorEdit } from 'vscode'; import { ICommandNameArgumentTypeMapping } from './commands'; @@ -22,10 +20,13 @@ export class CommandManager implements ICommandManager { * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ + // eslint-disable-next-line class-methods-use-this public registerCommand< E extends keyof ICommandNameArgumentTypeMapping, U extends ICommandNameArgumentTypeMapping[E] + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any >(command: E, callback: (...args: U) => any, thisArg?: any): Disposable { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return commands.registerCommand(command, callback as any, thisArg); } @@ -43,10 +44,13 @@ export class CommandManager implements ICommandManager { * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ + // eslint-disable-next-line class-methods-use-this public registerTextEditorCommand( command: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, - thisArg?: any + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + thisArg?: any, ): Disposable { return commands.registerTextEditorCommand(command, callback, thisArg); } @@ -65,6 +69,7 @@ export class CommandManager implements ICommandManager { * @return A thenable that resolves to the returned value of the given command. `undefined` when * the command handler function doesn't return anything. */ + // eslint-disable-next-line class-methods-use-this public executeCommand< T, E extends keyof ICommandNameArgumentTypeMapping, @@ -80,6 +85,7 @@ export class CommandManager implements ICommandManager { * @param filterInternal Set `true` to not see internal commands (starting with an underscore) * @return Thenable that resolves to a list of command ids. */ + // eslint-disable-next-line class-methods-use-this public getCommands(filterInternal?: boolean): Thenable { return commands.getCommands(filterInternal); } diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 3ef4263c8670..b43dc0a1e4a4 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -3,100 +3,76 @@ 'use strict'; -import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; +import { CancellationToken, Position, TestItem, TextDocument, Uri } from 'vscode'; import { Commands as LSCommands } from '../../activation/commands'; -import { Commands as DSCommands } from '../../datascience/constants'; -import { KernelSpecInterpreter } from '../../datascience/jupyter/kernels/kernelSelector'; -import { INotebookModel, ISwitchKernelOptions } from '../../datascience/types'; -import { CommandSource } from '../../testing/common/constants'; -import { TestFunction, TestsToRun } from '../../testing/common/types'; -import { TestDataItem, TestWorkspaceFolder } from '../../testing/types'; -import { Commands } from '../constants'; -import { Channel } from './types'; +import { Channel, Commands, CommandSource } from '../constants'; +import { CreateEnvironmentOptions } from '../../pythonEnvironments/creation/proposed.createEnvApis'; export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; /** * Mapping between commands and list or arguments. * These commands do NOT have any arguments. - * @interface ICommandNameWithoutArgumentTypeMapping */ interface ICommandNameWithoutArgumentTypeMapping { - [Commands.SwitchToInsidersDaily]: []; - [Commands.SwitchToInsidersWeekly]: []; + [Commands.InstallPythonOnMac]: []; + [Commands.InstallJupyter]: []; + [Commands.InstallPythonOnLinux]: []; + [Commands.InstallPython]: []; [Commands.ClearWorkspaceInterpreter]: []; - [Commands.ResetInterpreterSecurityStorage]: []; - [Commands.SwitchOffInsidersChannel]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; - [Commands.Run_Linter]: []; - [Commands.Enable_Linter]: []; ['workbench.action.showCommands']: []; ['workbench.action.debug.continue']: []; ['workbench.action.debug.stepOver']: []; ['workbench.action.debug.stop']: []; ['workbench.action.reloadWindow']: []; ['workbench.action.closeActiveEditor']: []; + ['workbench.action.terminal.focus']: []; ['editor.action.formatDocument']: []; ['editor.action.rename']: []; - ['python.datascience.selectJupyterInterpreter']: []; [Commands.ViewOutput]: []; - [Commands.Set_Linter]: []; [Commands.Start_REPL]: []; - [Commands.Enable_SourceMap_Support]: []; [Commands.Exec_Selection_In_Terminal]: []; [Commands.Exec_Selection_In_Django_Shell]: []; [Commands.Create_Terminal]: []; - [Commands.Tests_View_UI]: []; - [Commands.Tests_Ask_To_Stop_Discovery]: []; - [Commands.Tests_Ask_To_Stop_Test]: []; - [Commands.Tests_Discovering]: []; [Commands.PickLocalProcess]: []; - [DSCommands.RunCurrentCell]: []; - [DSCommands.RunCurrentCellAdvance]: []; - [DSCommands.ExecSelectionInInteractiveWindow]: []; - [DSCommands.SelectJupyterURI]: []; - [DSCommands.CreateNewInteractive]: []; - [DSCommands.UndoCells]: []; - [DSCommands.RedoCells]: []; - [DSCommands.RemoveAllCells]: []; - [DSCommands.InterruptKernel]: []; - [DSCommands.RestartKernel]: []; - [DSCommands.NotebookEditorUndoCells]: []; - [DSCommands.NotebookEditorRedoCells]: []; - [DSCommands.NotebookEditorRemoveAllCells]: []; - [DSCommands.NotebookEditorInterruptKernel]: []; - [DSCommands.NotebookEditorRestartKernel]: []; - [DSCommands.NotebookEditorRunAllCells]: []; - [DSCommands.NotebookEditorRunSelectedCell]: []; - [DSCommands.NotebookEditorAddCellBelow]: []; - [DSCommands.ExpandAllCells]: []; - [DSCommands.CollapseAllCells]: []; - [DSCommands.ExportOutputAsNotebook]: []; - [DSCommands.AddCellBelow]: []; - [DSCommands.CreateNewNotebook]: []; - [Commands.OpenStartPage]: []; - [LSCommands.ClearAnalyisCache]: []; + [Commands.ClearStorage]: []; + [Commands.CreateNewFile]: []; + [Commands.ReportIssue]: []; [LSCommands.RestartLS]: []; } +export type AllCommands = keyof ICommandNameArgumentTypeMapping; + /** * Mapping between commands and list of arguments. * Used to provide strong typing for command & args. - * @export - * @interface ICommandNameArgumentTypeMapping - * @extends {ICommandNameWithoutArgumentTypeMapping} */ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping { + [Commands.CopyTestId]: [TestItem]; + [Commands.Create_Environment]: [CreateEnvironmentOptions]; ['vscode.openWith']: [Uri, string]; ['workbench.action.quickOpen']: [string]; - ['workbench.extensions.installExtension']: [Uri | 'ms-python.python']; + ['workbench.action.openWalkthrough']: [string | { category: string; step: string }, boolean | undefined]; + ['workbench.extensions.installExtension']: [ + Uri | string, + ( + | { + installOnlyNewlyAddedFromExtensionPackVSIX?: boolean; + installPreReleaseVersion?: boolean; + donotSync?: boolean; + } + | undefined + ), + ]; ['workbench.action.files.openFolder']: []; ['workbench.action.openWorkspace']: []; + ['workbench.action.openSettings']: [string]; ['setContext']: [string, boolean] | ['python.vscode.channel', Channel]; ['python.reloadVSCode']: [string]; ['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }]; - ['python._loadLanguageServerExtension']: {}[]; + ['python._loadLanguageServerExtension']: []; ['python.SelectAndInsertDebugConfiguration']: [TextDocument, Position, CancellationToken]; ['vscode.open']: [Uri]; ['notebook.execute']: []; @@ -108,96 +84,29 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['vscode.open']: [Uri]; ['workbench.action.files.saveAs']: [Uri]; ['workbench.action.files.save']: [Uri]; + ['jupyter.opennotebook']: [undefined | Uri, undefined | CommandSource]; + ['jupyter.runallcells']: [Uri]; + ['extension.open']: [string]; + ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string; extensionData?: string }]; [Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]]; - [Commands.Build_Workspace_Symbols]: [boolean, CancellationToken]; - [Commands.Sort_Imports]: [undefined, Uri]; + [Commands.TriggerEnvironmentSelection]: [undefined | Uri]; + [Commands.Start_Native_REPL]: [undefined | Uri]; + [Commands.Exec_In_REPL]: [undefined | Uri]; + [Commands.Exec_In_REPL_Enter]: [undefined | Uri]; + [Commands.Exec_In_IW_Enter]: [undefined | Uri]; [Commands.Exec_In_Terminal]: [undefined, Uri]; [Commands.Exec_In_Terminal_Icon]: [undefined, Uri]; - [Commands.Tests_ViewOutput]: [undefined, CommandSource]; - [Commands.Tests_Select_And_Run_File]: [undefined, CommandSource]; - [Commands.Tests_Run_Current_File]: [undefined, CommandSource]; - [Commands.Tests_Stop]: [undefined, Uri]; - [Commands.Test_Reveal_Test_Item]: [TestDataItem]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.Tests_Run]: [ - undefined | TestWorkspaceFolder, - undefined | CommandSource, - undefined | Uri, - undefined | TestsToRun - ]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.Tests_Debug]: [ - undefined | TestWorkspaceFolder, - undefined | CommandSource, - undefined | Uri, - undefined | TestsToRun - ]; - [Commands.Tests_Run_Parametrized]: [undefined, undefined | CommandSource, Uri, TestFunction[], boolean]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.Tests_Discover]: [undefined | TestWorkspaceFolder, undefined | CommandSource, undefined | Uri]; - [Commands.Tests_Run_Failed]: [undefined, CommandSource, Uri]; - [Commands.Tests_Select_And_Debug_Method]: [undefined, CommandSource, Uri]; - [Commands.Tests_Select_And_Run_Method]: [undefined, CommandSource, Uri]; + [Commands.Debug_In_Terminal]: [Uri]; [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri]; - [Commands.Tests_Picker_UI]: [undefined, undefined | CommandSource, Uri, TestFunction[]]; - [Commands.Tests_Picker_UI_Debug]: [undefined, undefined | CommandSource, Uri, TestFunction[]]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.runTestNode]: [TestDataItem]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.debugTestNode]: [TestDataItem]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.openTestNodeInEditor]: [TestDataItem]; - [Commands.navigateToTestFile]: [Uri, TestDataItem, boolean]; - [Commands.navigateToTestFunction]: [Uri, TestDataItem, boolean]; - [Commands.navigateToTestSuite]: [Uri, TestDataItem, boolean]; - [DSCommands.ExportFileAndOutputAsNotebook]: [Uri]; - [DSCommands.RunAllCells]: [Uri]; - [DSCommands.RunCell]: [Uri, number, number, number, number]; - [DSCommands.RunAllCellsAbove]: [Uri, number, number]; - [DSCommands.RunCellAndAllBelow]: [Uri, number, number]; - [DSCommands.RunAllCellsAbovePalette]: []; - [DSCommands.RunCellAndAllBelowPalette]: []; - [DSCommands.DebugCurrentCellPalette]: []; - [DSCommands.RunToLine]: [Uri, number, number]; - [DSCommands.RunFromLine]: [Uri, number, number]; - [DSCommands.ImportNotebook]: [undefined | Uri, undefined | CommandSource]; - [DSCommands.ImportNotebookFile]: [undefined | Uri, undefined | CommandSource]; - [DSCommands.OpenNotebook]: [undefined | Uri, undefined | CommandSource]; - [DSCommands.OpenNotebookInPreviewEditor]: [undefined | Uri]; - [DSCommands.ExportFileAsNotebook]: [undefined | Uri, undefined | CommandSource]; - [DSCommands.RunFileInInteractiveWindows]: [Uri]; - [DSCommands.DebugFileInInteractiveWindows]: [Uri]; - [DSCommands.DebugCell]: [Uri, number, number, number, number]; - [DSCommands.DebugStepOver]: []; - [DSCommands.DebugStop]: []; - [DSCommands.DebugContinue]: []; - [DSCommands.RunCurrentCellAndAddBelow]: [Uri]; - [DSCommands.InsertCellBelowPosition]: []; - [DSCommands.InsertCellBelow]: []; - [DSCommands.InsertCellAbove]: []; - [DSCommands.DeleteCells]: []; - [DSCommands.SelectCell]: []; - [DSCommands.SelectCellContents]: []; - [DSCommands.ExtendSelectionByCellAbove]: []; - [DSCommands.ExtendSelectionByCellBelow]: []; - [DSCommands.MoveCellsUp]: []; - [DSCommands.MoveCellsDown]: []; - [DSCommands.ChangeCellToMarkdown]: []; - [DSCommands.ChangeCellToCode]: []; - [DSCommands.ScrollToCell]: [Uri, string]; - [DSCommands.ViewJupyterOutput]: []; - [DSCommands.ExportAsPythonScript]: [INotebookModel]; - [DSCommands.ExportToHTML]: [INotebookModel, string | undefined]; - [DSCommands.ExportToPDF]: [INotebookModel, string | undefined]; - [DSCommands.Export]: [Uri | INotebookModel, string | undefined]; - [DSCommands.SetJupyterKernel]: [KernelSpecInterpreter, Uri, undefined | Uri]; - [DSCommands.SwitchJupyterKernel]: [ISwitchKernelOptions | undefined]; - [DSCommands.SelectJupyterCommandLine]: [undefined | Uri]; - [DSCommands.SaveNotebookNonCustomEditor]: [Uri]; - [DSCommands.SaveAsNotebookNonCustomEditor]: [Uri, Uri]; - [DSCommands.OpenNotebookNonCustomEditor]: [Uri]; - [DSCommands.GatherQuality]: [string]; - [DSCommands.LatestExtension]: [string]; - [DSCommands.EnableLoadingWidgetsFrom3rdPartySource]: [undefined | never]; - [DSCommands.TrustNotebook]: [undefined | never | Uri]; + [Commands.Tests_CopilotSetup]: [undefined | Uri]; + ['workbench.view.testing.focus']: []; + ['cursorMove']: [ + { + to: string; + by: string; + value: number; + }, + ]; + ['cursorEnd']: []; + ['python-envs.createTerminal']: [undefined | Uri]; } diff --git a/src/client/common/application/commands/createPythonFile.ts b/src/client/common/application/commands/createPythonFile.ts new file mode 100644 index 000000000000..10f388856896 --- /dev/null +++ b/src/client/common/application/commands/createPythonFile.ts @@ -0,0 +1,29 @@ +import { injectable, inject } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { Commands } from '../../constants'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../types'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IDisposableRegistry } from '../../types'; + +@injectable() +export class CreatePythonFileCommandHandler implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.CreateNewFile, this.createPythonFile, this)); + } + + public async createPythonFile(): Promise { + const newFile = await this.workspaceService.openTextDocument({ language: 'python' }); + this.appShell.showTextDocument(newFile); + sendTelemetryEvent(EventName.CREATE_NEW_FILE_COMMAND); + } +} diff --git a/src/client/common/application/commands/reloadCommand.ts b/src/client/common/application/commands/reloadCommand.ts index 36fded88b797..ebad15dbb70d 100644 --- a/src/client/common/application/commands/reloadCommand.ts +++ b/src/client/common/application/commands/reloadCommand.ts @@ -14,16 +14,17 @@ import { IApplicationShell, ICommandManager } from '../types'; */ @injectable() export class ReloadVSCodeCommandHandler implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; constructor( @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IApplicationShell) private readonly appShell: IApplicationShell + @inject(IApplicationShell) private readonly appShell: IApplicationShell, ) {} public async activate(): Promise { this.commandManager.registerCommand('python.reloadVSCode', this.onReloadVSCode, this); } private async onReloadVSCode(message: string) { - const item = await this.appShell.showInformationMessage(message, Common.reload()); - if (item === Common.reload()) { + const item = await this.appShell.showInformationMessage(message, Common.reload); + if (item === Common.reload) { this.commandManager.executeCommand('workbench.action.reloadWindow').then(noop, noop); } } diff --git a/src/client/common/application/commands/reportIssueCommand.ts b/src/client/common/application/commands/reportIssueCommand.ts new file mode 100644 index 000000000000..9ae099e44b4f --- /dev/null +++ b/src/client/common/application/commands/reportIssueCommand.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as os from 'os'; +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { isEqual } from 'lodash'; +import * as fs from '../../platform/fs-paths'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from '../types'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { Commands } from '../../constants'; +import { IConfigurationService, IPythonSettings } from '../../types'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; +import { PythonSettings } from '../../configSettings'; +import { SystemVariables } from '../../variables/systemVariables'; +import { getExtensions } from '../../vscodeApis/extensionsApi'; + +/** + * Allows the user to report an issue related to the Python extension using our template. + */ +@injectable() +export class ReportIssueCommandHandler implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly packageJSONSettings: any; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, + @inject(IApplicationEnvironment) appEnvironment: IApplicationEnvironment, + ) { + this.packageJSONSettings = appEnvironment.packageJson?.contributes?.configuration?.properties; + } + + public async activate(): Promise { + this.commandManager.registerCommand(Commands.ReportIssue, this.openReportIssue, this); + } + + private argSettingsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_settings.json'); + + private templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md'); + + private userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md'); + + public async openReportIssue(): Promise { + const settings: IPythonSettings = this.configurationService.getSettings(); + const argSettings = JSON.parse(await fs.readFile(this.argSettingsPath, 'utf8')); + let userSettings = ''; + const keys: [keyof IPythonSettings] = Object.keys(settings) as [keyof IPythonSettings]; + keys.forEach((property) => { + const argSetting = argSettings[property]; + if (argSetting) { + if (typeof argSetting === 'object') { + let propertyHeaderAdded = false; + const argSettingsDict = (settings[property] as unknown) as Record; + if (typeof argSettingsDict === 'object') { + Object.keys(argSetting).forEach((item) => { + const prop = argSetting[item]; + if (prop) { + const defaultValue = this.getDefaultValue(`${property}.${item}`); + if (defaultValue === undefined || !isEqual(defaultValue, argSettingsDict[item])) { + if (!propertyHeaderAdded) { + userSettings = userSettings.concat(os.EOL, property, os.EOL); + propertyHeaderAdded = true; + } + const value = + prop === true ? JSON.stringify(argSettingsDict[item]) : '""'; + userSettings = userSettings.concat('• ', item, ': ', value, os.EOL); + } + } + }); + } + } else { + const defaultValue = this.getDefaultValue(property); + if (defaultValue === undefined || !isEqual(defaultValue, settings[property])) { + const value = argSetting === true ? JSON.stringify(settings[property]) : '""'; + userSettings = userSettings.concat(os.EOL, property, ': ', value, os.EOL); + } + } + } + }); + const template = await fs.readFile(this.templatePath, 'utf8'); + const userTemplate = await fs.readFile(this.userDataTemplatePath, 'utf8'); + const interpreter = await this.interpreterService.getActiveInterpreter(); + const pythonVersion = interpreter?.version?.raw ?? ''; + const languageServer = + this.workspaceService.getConfiguration('python').get('languageServer') || 'Not Found'; + const virtualEnvKind = interpreter?.envType || EnvironmentType.Unknown; + + const hasMultipleFolders = (this.workspaceService.workspaceFolders?.length ?? 0) > 1; + const hasMultipleFoldersText = + hasMultipleFolders && userSettings !== '' + ? `Multiroot scenario, following user settings may not apply:${os.EOL}` + : ''; + + const installedExtensions = getExtensions() + .filter((extension) => !extension.id.startsWith('vscode.')) + .sort((a, b) => { + if (a.packageJSON.name && b.packageJSON.name) { + return a.packageJSON.name.localeCompare(b.packageJSON.name); + } + return a.id.localeCompare(b.id); + }) + .map((extension) => { + let publisher: string = extension.packageJSON.publisher as string; + if (publisher) { + publisher = publisher.substring(0, 3); + } + return `|${extension.packageJSON.name}|${publisher}|${extension.packageJSON.version}|`; + }); + + await this.commandManager.executeCommand('workbench.action.openIssueReporter', { + extensionId: 'ms-python.python', + issueBody: template, + extensionData: userTemplate.format( + pythonVersion, + virtualEnvKind, + languageServer, + hasMultipleFoldersText, + userSettings, + installedExtensions.join('\n'), + ), + }); + sendTelemetryEvent(EventName.USE_REPORT_ISSUE_COMMAND, undefined, {}); + } + + private getDefaultValue(settingKey: string) { + if (!this.packageJSONSettings) { + return undefined; + } + const resource = PythonSettings.getSettingsUriAndTarget(undefined, this.workspaceService).uri; + const systemVariables = new SystemVariables(resource, undefined, this.workspaceService); + return systemVariables.resolveAny(this.packageJSONSettings[`python.${settingKey}`]?.default); + } +} diff --git a/src/client/common/application/contextKeyManager.ts b/src/client/common/application/contextKeyManager.ts new file mode 100644 index 000000000000..388fcf4a3841 --- /dev/null +++ b/src/client/common/application/contextKeyManager.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ExtensionContextKey } from './contextKeys'; +import { ICommandManager, IContextKeyManager } from './types'; + +@injectable() +export class ContextKeyManager implements IContextKeyManager { + private values: Map = new Map(); + + constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager) {} + + public async setContext(key: ExtensionContextKey, value: boolean): Promise { + if (this.values.get(key) === value) { + return Promise.resolve(); + } + this.values.set(key, value); + return this.commandManager.executeCommand('setContext', key, value); + } +} diff --git a/src/client/common/application/contextKeys.ts b/src/client/common/application/contextKeys.ts new file mode 100644 index 000000000000..d6249f05eaec --- /dev/null +++ b/src/client/common/application/contextKeys.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export enum ExtensionContextKey { + showInstallPythonTile = 'showInstallPythonTile', + HasFailedTests = 'hasFailedTests', + RefreshingTests = 'refreshingTests', + IsJupyterInstalled = 'isJupyterInstalled', +} diff --git a/src/client/common/application/customEditorService.ts b/src/client/common/application/customEditorService.ts deleted file mode 100644 index 35687c8408a2..000000000000 --- a/src/client/common/application/customEditorService.ts +++ /dev/null @@ -1,85 +0,0 @@ -// 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 * as vscode from 'vscode'; -import { DataScience } from '../../common/utils/localize'; - -import { EXTENSION_ROOT_DIR, UseCustomEditorApi } from '../constants'; -import { traceError } from '../logger'; -import { IFileSystem } from '../platform/types'; -import { noop } from '../utils/misc'; -import { CustomEditorProvider, IApplicationEnvironment, ICommandManager, ICustomEditorService } from './types'; - -@injectable() -export class CustomEditorService implements ICustomEditorService { - constructor( - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(UseCustomEditorApi) private readonly useCustomEditorApi: boolean, - @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, - @inject(IFileSystem) private readonly fileSystem: IFileSystem - ) { - this.verifyPackageJson().catch((e) => traceError(`Error rewriting package json: `, e)); - } - - public registerCustomEditorProvider( - viewType: string, - provider: CustomEditorProvider, - options?: { - readonly webviewOptions?: vscode.WebviewPanelOptions; - readonly supportsMultipleEditorsPerDocument?: boolean; - } - ): vscode.Disposable { - if (this.useCustomEditorApi) { - // tslint:disable-next-line: no-any - return (vscode.window as any).registerCustomEditorProvider(viewType, provider, options); - } else { - return { dispose: noop }; - } - } - - public async openEditor(file: vscode.Uri, viewType: string): Promise { - if (this.useCustomEditorApi) { - await this.commandManager.executeCommand('vscode.openWith', file, viewType); - } - } - - private async verifyPackageJson(): Promise { - // Double check the package json has the necessary entries for contributing a custom editor. Note - // we have to actually read it because appEnvironment.packageJson is the webpacked version - const packageJson = JSON.parse(await this.fileSystem.readFile(path.join(EXTENSION_ROOT_DIR, 'package.json'))); - if (this.useCustomEditorApi && !packageJson.contributes?.customEditors) { - return this.addCustomEditors(packageJson); - } else if (!this.useCustomEditorApi && packageJson.contributes.customEditors) { - return this.removeCustomEditors(); - } - } - - // tslint:disable-next-line: no-any - private async addCustomEditors(currentPackageJson: any) { - // tslint:disable-next-line:no-require-imports no-var-requires - const _mergeWith = require('lodash/mergeWith') as typeof import('lodash/mergeWith'); - const improvedContents = await this.fileSystem.readFile(path.join(EXTENSION_ROOT_DIR, 'customEditor.json')); - const improved = _mergeWith({ ...currentPackageJson }, JSON.parse(improvedContents), (l, r) => { - if (Array.isArray(l) && Array.isArray(r)) { - return [...l, ...r]; - } - }); - await this.fileSystem.writeFile( - path.join(EXTENSION_ROOT_DIR, 'package.json'), - JSON.stringify(improved, null, 4) - ); - this.commandManager.executeCommand('python.reloadVSCode', DataScience.reloadCustomEditor()); - } - private async removeCustomEditors() { - // Note, to put it back, use the shipped version. This packageJson is required into the product - // so it's packed by webpack into the source. - const original = { ...this.appEnvironment.packageJson }; - await this.fileSystem.writeFile( - path.join(EXTENSION_ROOT_DIR, 'package.json'), - JSON.stringify(original, null, 4) - ); - this.commandManager.executeCommand('python.reloadVSCode', DataScience.reloadCustomEditor()); - } -} diff --git a/src/client/common/application/debugService.ts b/src/client/common/application/debugService.ts index f553b6cd6f5c..7de039e946c2 100644 --- a/src/client/common/application/debugService.ts +++ b/src/client/common/application/debugService.ts @@ -13,9 +13,10 @@ import { DebugConsole, DebugSession, DebugSessionCustomEvent, + DebugSessionOptions, Disposable, Event, - WorkspaceFolder + WorkspaceFolder, } from 'vscode'; import { IDebugService } from './types'; @@ -28,7 +29,7 @@ export class DebugService implements IDebugService { public get activeDebugSession(): DebugSession | undefined { return debug.activeDebugSession; } - public get breakpoints(): Breakpoint[] { + public get breakpoints(): readonly Breakpoint[] { return debug.breakpoints; } public get onDidChangeActiveDebugSession(): Event { @@ -46,18 +47,18 @@ export class DebugService implements IDebugService { public get onDidChangeBreakpoints(): Event { return debug.onDidChangeBreakpoints; } - // tslint:disable-next-line:no-any + public registerDebugConfigurationProvider(debugType: string, provider: any): Disposable { return debug.registerDebugConfigurationProvider(debugType, provider); } - // tslint:disable-next-line:no-any + public registerDebugAdapterTrackerFactory(debugType: string, provider: any): Disposable { return debug.registerDebugAdapterTrackerFactory(debugType, provider); } public startDebugging( folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, - parentSession?: DebugSession + parentSession?: DebugSession | DebugSessionOptions, ): Thenable { return debug.startDebugging(folder, nameOrConfiguration, parentSession); } @@ -69,7 +70,7 @@ export class DebugService implements IDebugService { } public registerDebugAdapterDescriptorFactory( debugType: string, - factory: DebugAdapterDescriptorFactory + factory: DebugAdapterDescriptorFactory, ): Disposable { return debug.registerDebugAdapterDescriptorFactory(debugType, factory); } diff --git a/src/client/common/application/debugSessionTelemetry.ts b/src/client/common/application/debugSessionTelemetry.ts deleted file mode 100644 index 47aac873a835..000000000000 --- a/src/client/common/application/debugSessionTelemetry.ts +++ /dev/null @@ -1,78 +0,0 @@ -// 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 { IExtensionSingleActivationService } from '../../activation/types'; -import { AttachRequestArguments, ConsoleType, LaunchRequestArguments, TriggerType } from '../../debugger/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { IDisposableRegistry } from '../types'; -import { StopWatch } from '../utils/stopWatch'; -import { IDebugService } from './types'; - -class TelemetryTracker implements DebugAdapterTracker { - private timer = new StopWatch(); - private readonly trigger: TriggerType = 'launch'; - private readonly console: ConsoleType | undefined; - - constructor(session: DebugSession) { - this.trigger = session.configuration.type as TriggerType; - const debugConfiguration = session.configuration as Partial; - this.console = debugConfiguration.console; - } - - public onWillStartSession() { - this.sendTelemetry(EventName.DEBUG_SESSION_START); - } - - // tslint:disable-next-line:no-any - public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { - if (message.type === 'response') { - const response = message as DebugProtocol.Response; - if (response.command === 'configurationDone') { - // "configurationDone" response is sent immediately after user code starts running. - this.sendTelemetry(EventName.DEBUG_SESSION_USER_CODE_RUNNING); - } - } - } - - public onWillStopSession() { - this.sendTelemetry(EventName.DEBUG_SESSION_STOP); - } - - public onError?(_error: Error) { - this.sendTelemetry(EventName.DEBUG_SESSION_ERROR); - } - - private sendTelemetry(eventName: EventName) { - if (eventName === EventName.DEBUG_SESSION_START) { - this.timer.reset(); - } - const telemetryProps = { - trigger: this.trigger, - console: this.console - }; - sendTelemetryEvent(eventName, this.timer.elapsedTime, telemetryProps); - } -} - -@injectable() -export class DebugSessionTelemetry implements DebugAdapterTrackerFactory, IExtensionSingleActivationService { - constructor( - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IDebugService) debugService: IDebugService - ) { - disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); - } - - public async activate(): Promise { - // We actually register in the constructor. Not necessary to do it here - } - - public createDebugAdapterTracker(session: DebugSession): ProviderResult { - return new TelemetryTracker(session); - } -} diff --git a/src/client/common/application/documentManager.ts b/src/client/common/application/documentManager.ts index d3fe7dd2140e..617d335e402b 100644 --- a/src/client/common/application/documentManager.ts +++ b/src/client/common/application/documentManager.ts @@ -16,13 +16,11 @@ import { ViewColumn, window, workspace, - WorkspaceEdit + WorkspaceEdit, } from 'vscode'; import { IDocumentManager } from './types'; -// tslint:disable:no-any unified-signatures - @injectable() export class DocumentManager implements IDocumentManager { public get textDocuments(): readonly TextDocument[] { @@ -31,7 +29,7 @@ export class DocumentManager implements IDocumentManager { public get activeTextEditor(): TextEditor | undefined { return window.activeTextEditor; } - public get visibleTextEditors(): TextEditor[] { + public get visibleTextEditors(): readonly TextEditor[] { return window.visibleTextEditors; } public get onDidChangeActiveTextEditor(): Event { @@ -40,7 +38,7 @@ export class DocumentManager implements IDocumentManager { public get onDidChangeTextDocument(): Event { return workspace.onDidChangeTextDocument; } - public get onDidChangeVisibleTextEditors(): Event { + public get onDidChangeVisibleTextEditors(): Event { return window.onDidChangeVisibleTextEditors; } public get onDidChangeTextEditorSelection(): Event { diff --git a/src/client/common/application/extensions.ts b/src/client/common/application/extensions.ts index 828f2ddcf804..e4b8f5bce73d 100644 --- a/src/client/common/application/extensions.ts +++ b/src/client/common/application/extensions.ts @@ -1,15 +1,28 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. 'use strict'; -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { Event, Extension, extensions } from 'vscode'; +import * as stacktrace from 'stack-trace'; +import * as path from 'path'; import { IExtensions } from '../types'; +import { IFileSystem } from '../platform/types'; +import { EXTENSION_ROOT_DIR } from '../constants'; +/** + * Provides functions for tracking the list of extensions that VSCode has installed. + */ @injectable() export class Extensions implements IExtensions { - // tslint:disable-next-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _cachedExtensions?: readonly Extension[]; + + constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any public get all(): readonly Extension[] { return extensions.all; } @@ -18,8 +31,70 @@ export class Extensions implements IExtensions { return extensions.onDidChange; } - // tslint:disable-next-line:no-any - public getExtension(extensionId: any) { + public getExtension(extensionId: string): Extension | undefined { return extensions.getExtension(extensionId); } + + private get cachedExtensions() { + if (!this._cachedExtensions) { + this._cachedExtensions = extensions.all; + extensions.onDidChange(() => { + this._cachedExtensions = extensions.all; + }); + } + return this._cachedExtensions; + } + + /** + * Code borrowed from: + * https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts + */ + public async determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + const { stack } = new Error(); + if (stack) { + const pythonExtRoot = path.join(EXTENSION_ROOT_DIR.toLowerCase(), path.sep); + const frames = stack + .split('\n') + .map((f) => { + const result = /\((.*)\)/.exec(f); + if (result) { + return result[1]; + } + return undefined; + }) + .filter((item) => item && !item.toLowerCase().startsWith(pythonExtRoot)) + .filter((item) => + // Use cached list of extensions as we need this to be fast. + this.cachedExtensions.some( + (ext) => item!.includes(ext.extensionUri.path) || item!.includes(ext.extensionUri.fsPath), + ), + ) as string[]; + stacktrace.parse(new Error('Ex')).forEach((item) => { + const fileName = item.getFileName(); + if (fileName && !fileName.toLowerCase().startsWith(pythonExtRoot)) { + frames.push(fileName); + } + }); + for (const frame of frames) { + // 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.pathExists(possiblePackageJson)) { + const text = await this.fs.readFile(possiblePackageJson); + try { + const json = JSON.parse(text); + return { extensionId: `${json.publisher}.${json.name}`, displayName: json.displayName }; + } catch { + // If parse fails, then not an extension. + } + } + last = dirName; + dirName = path.dirname(dirName); + } + } + } + return { extensionId: 'unknown', displayName: 'unknown' }; + } } diff --git a/src/client/common/application/notebook.ts b/src/client/common/application/notebook.ts deleted file mode 100644 index 21936c3d5df8..000000000000 --- a/src/client/common/application/notebook.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, GlobPattern } from 'vscode'; -import type { - notebook, - NotebookCellsChangeEvent as VSCNotebookCellsChangeEvent, - NotebookContentProvider, - NotebookDocument, - NotebookEditor, - NotebookKernel, - NotebookOutputRenderer, - NotebookOutputSelector -} from 'vscode-proposed'; -import { UseProposedApi } from '../constants'; -import { IDisposableRegistry } from '../types'; -import { - IApplicationEnvironment, - IVSCodeNotebook, - NotebookCellLanguageChangeEvent, - NotebookCellOutputsChangeEvent, - NotebookCellsChangeEvent -} from './types'; - -@injectable() -export class VSCodeNotebook implements IVSCodeNotebook { - public get onDidChangeActiveNotebookEditor(): Event { - return this.canUseNotebookApi - ? this.notebook.onDidChangeActiveNotebookEditor - : new EventEmitter().event; - } - public get onDidOpenNotebookDocument(): Event { - return this.canUseNotebookApi - ? this.notebook.onDidOpenNotebookDocument - : new EventEmitter().event; - } - public get onDidCloseNotebookDocument(): Event { - return this.canUseNotebookApi - ? this.notebook.onDidCloseNotebookDocument - : new EventEmitter().event; - } - public get notebookDocuments(): ReadonlyArray { - return this.canUseNotebookApi ? this.notebook.notebookDocuments : []; - } - public get notebookEditors() { - return this.canUseNotebookApi ? this.notebook.visibleNotebookEditors : []; - } - public get onDidChangeNotebookDocument(): Event< - NotebookCellsChangeEvent | NotebookCellOutputsChangeEvent | NotebookCellLanguageChangeEvent - > { - return this.canUseNotebookApi - ? this._onDidChangeNotebookDocument.event - : new EventEmitter< - NotebookCellsChangeEvent | NotebookCellOutputsChangeEvent | NotebookCellLanguageChangeEvent - >().event; - } - public get activeNotebookEditor(): NotebookEditor | undefined { - if (!this.useProposedApi) { - return; - } - return this.notebook.activeNotebookEditor; - } - private get notebook() { - if (!this._notebook) { - // tslint:disable-next-line: no-require-imports - this._notebook = require('vscode').notebook; - } - return this._notebook!; - } - private readonly _onDidChangeNotebookDocument = new EventEmitter< - NotebookCellsChangeEvent | NotebookCellOutputsChangeEvent | NotebookCellLanguageChangeEvent - >(); - private addedEventHandlers?: boolean; - private _notebook?: typeof notebook; - private readonly canUseNotebookApi?: boolean; - private readonly handledCellChanges = new WeakSet(); - constructor( - @inject(UseProposedApi) private readonly useProposedApi: boolean, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IApplicationEnvironment) readonly env: IApplicationEnvironment - ) { - if (this.useProposedApi && this.env.channel === 'insiders') { - this.addEventHandlers(); - this.canUseNotebookApi = true; - } - } - public registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider): Disposable { - return this.notebook.registerNotebookContentProvider(notebookType, provider); - } - public registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable { - return this.notebook.registerNotebookKernel(id, selectors, kernel); - } - public registerNotebookOutputRenderer( - id: string, - outputSelector: NotebookOutputSelector, - renderer: NotebookOutputRenderer - ): Disposable { - return this.notebook.registerNotebookOutputRenderer(id, outputSelector, renderer); - } - private addEventHandlers() { - if (this.addedEventHandlers) { - return; - } - this.addedEventHandlers = true; - this.disposables.push( - ...[ - this.notebook.onDidChangeCellLanguage((e) => - this._onDidChangeNotebookDocument.fire({ ...e, type: 'changeCellLanguage' }) - ), - this.notebook.onDidChangeCellOutputs((e) => - this._onDidChangeNotebookDocument.fire({ ...e, type: 'changeCellOutputs' }) - ), - this.notebook.onDidChangeNotebookCells((e) => { - if (this.handledCellChanges.has(e)) { - return; - } - this.handledCellChanges.add(e); - this._onDidChangeNotebookDocument.fire({ ...e, type: 'changeCells' }); - }) - ] - ); - } -} diff --git a/src/client/common/application/progressService.ts b/src/client/common/application/progressService.ts new file mode 100644 index 000000000000..fb19cad1136c --- /dev/null +++ b/src/client/common/application/progressService.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ProgressOptions } from 'vscode'; +import { Deferred, createDeferred } from '../utils/async'; +import { IApplicationShell } from './types'; + +export class ProgressService { + private deferred: Deferred | undefined; + + constructor(private readonly shell: IApplicationShell) {} + + public showProgress(options: ProgressOptions): void { + if (!this.deferred) { + this.createProgress(options); + } + } + + public hideProgress(): void { + if (this.deferred) { + this.deferred.resolve(); + this.deferred = undefined; + } + } + + private createProgress(options: ProgressOptions) { + this.shell.withProgress(options, () => { + this.deferred = createDeferred(); + return this.deferred.promise; + }); + } +} diff --git a/src/client/common/application/terminalManager.ts b/src/client/common/application/terminalManager.ts index 8fe6c067d0e6..dc2603e84a56 100644 --- a/src/client/common/application/terminalManager.ts +++ b/src/client/common/application/terminalManager.ts @@ -2,18 +2,58 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { Event, Terminal, TerminalOptions, window } from 'vscode'; +import { + Disposable, + Event, + EventEmitter, + Terminal, + TerminalOptions, + TerminalShellExecutionEndEvent, + TerminalShellIntegrationChangeEvent, + window, +} from 'vscode'; +import { traceLog } from '../../logging'; import { ITerminalManager } from './types'; @injectable() export class TerminalManager implements ITerminalManager { + private readonly didOpenTerminal = new EventEmitter(); + constructor() { + window.onDidOpenTerminal((terminal) => { + this.didOpenTerminal.fire(monkeyPatchTerminal(terminal)); + }); + } public get onDidCloseTerminal(): Event { return window.onDidCloseTerminal; } public get onDidOpenTerminal(): Event { - return window.onDidOpenTerminal; + return this.didOpenTerminal.event; } public createTerminal(options: TerminalOptions): Terminal { - return window.createTerminal(options); + return monkeyPatchTerminal(window.createTerminal(options)); + } + public onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable { + return window.onDidChangeTerminalShellIntegration(handler); + } + public onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable { + return window.onDidEndTerminalShellExecution(handler); + } + public onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); + } +} + +/** + * Monkeypatch the terminal to log commands sent. + */ +function monkeyPatchTerminal(terminal: Terminal) { + if (!(terminal as any).isPatched) { + const oldSendText = terminal.sendText.bind(terminal); + terminal.sendText = (text: string, addNewLine: boolean = true) => { + traceLog(`Send text to terminal: ${text}`); + return oldSendText(text, addNewLine); + }; + (terminal as any).isPatched = true; } + return terminal; } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 398f06a4ca35..34a95fb604f0 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; + import { Breakpoint, BreakpointsChangeEvent, @@ -14,6 +16,7 @@ import { DebugConsole, DebugSession, DebugSessionCustomEvent, + DebugSessionOptions, DecorationRenderOptions, Disposable, DocumentSelector, @@ -22,10 +25,11 @@ import { GlobPattern, InputBox, InputBoxOptions, + LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, - OutputChannel, Progress, ProgressOptions, QuickPick, @@ -36,6 +40,8 @@ import { StatusBarItem, Terminal, TerminalOptions, + TerminalShellExecutionEndEvent, + TerminalShellIntegrationChangeEvent, TextDocument, TextDocumentChangeEvent, TextDocumentShowOptions, @@ -47,43 +53,82 @@ import { TextEditorViewColumnChangeEvent, TreeView, TreeViewOptions, + UIKind, Uri, ViewColumn, - WebviewPanel, - WebviewPanelOptions, WindowState, WorkspaceConfiguration, WorkspaceEdit, WorkspaceFolder, WorkspaceFolderPickOptions, - WorkspaceFoldersChangeEvent + WorkspaceFoldersChangeEvent, } from 'vscode'; -import type { - NotebookCellLanguageChangeEvent as VSCNotebookCellLanguageChangeEvent, - NotebookCellOutputsChangeEvent as VSCNotebookCellOutputsChangeEvent, - NotebookCellsChangeEvent as VSCNotebookCellsChangeEvent, - NotebookContentProvider, - NotebookDocument, - NotebookEditor, - NotebookKernel, - NotebookOutputRenderer, - NotebookOutputSelector -} from 'vscode-proposed'; -import * as vsls from 'vsls/vscode'; - -import { IAsyncDisposable, Resource } from '../types'; + +import { Channel } from '../constants'; +import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; +import { ExtensionContextKey } from './contextKeys'; -// tslint:disable:no-any unified-signatures +export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; +} + +export interface TerminalExecutedCommand { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + /** + * The full command line that was executed, including both the command and the arguments. + */ + commandLine: string | undefined; + /** + * The current working directory that was reported by the shell. This will be a {@link Uri} + * if the string reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + /** + * The exit code reported by the shell. + */ + exitCode: number | undefined; + /** + * The output of the command when it has finished executing. This is the plain text shown in + * the terminal buffer and does not include raw escape sequences. Depending on the shell + * setup, this may include the command line as part of the output. + */ + output: string | undefined; +} export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { + /** + * An event that is emitted when a terminal with shell integration activated has completed + * executing a command. + * + * Note that this event will not fire if the executed command exits the shell, listen to + * {@link onDidCloseTerminal} to handle that case. + */ + readonly onDidExecuteTerminalCommand: Event | undefined; /** * An [event](#Event) which fires when the focus state of the current window * changes. The value of the event represents whether the window is focused. */ readonly onDidChangeWindowState: Event; + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + readonly onDidWriteTerminalData: Event; + showInformationMessage(message: string, ...items: string[]): Thenable; /** @@ -235,7 +280,7 @@ export interface IApplicationShell { showQuickPick( items: string[] | Thenable, options?: QuickPickOptions, - token?: CancellationToken + token?: CancellationToken, ): Thenable; /** @@ -249,7 +294,7 @@ export interface IApplicationShell { showQuickPick( items: T[] | Thenable, options?: QuickPickOptions, - token?: CancellationToken + token?: CancellationToken, ): Thenable; /** @@ -283,6 +328,19 @@ export interface IApplicationShell { */ showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; + /** + * Show the given document in a text editor. A {@link ViewColumn column} can be provided + * to control where the editor is being shown. Might change the {@link window.activeTextEditor active editor}. + * + * @param document A text document to be shown. + * @param column A view column in which the {@link TextEditor editor} should be shown. The default is the {@link ViewColumn.Active active}, other values + * are adjusted to be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is not adjusted. Use {@linkcode ViewColumn.Beside} + * to open the editor to the side of the currently active one. + * @param preserveFocus When `true` the editor will not take focus. + * @return A promise that resolves to an {@link TextEditor editor}. + */ + showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; + /** * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list * of items of type T. @@ -330,6 +388,7 @@ export interface IApplicationShell { * @param hideWhenDone Thenable on which completion (resolve or reject) the message will be disposed. * @return A disposable which hides the status bar message. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any setStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable; /** @@ -351,7 +410,7 @@ export interface IApplicationShell { * @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. */ - createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + createStatusBarItem(alignment?: StatusBarAlignment, priority?: number, id?: string): StatusBarItem; /** * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. * Returns `undefined` if no folder is open. @@ -382,7 +441,7 @@ export interface IApplicationShell { */ withProgress( options: ProgressOptions, - task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, ): Thenable; /** @@ -409,7 +468,7 @@ export interface IApplicationShell { */ withProgressCustomIcon( icon: string, - task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, ): Thenable; /** @@ -425,7 +484,8 @@ export interface IApplicationShell { * * @param name Human-readable string which will be used to represent the channel in the UI. */ - createOutputChannel(name: string): OutputChannel; + createOutputChannel(name: string): LogOutputChannel; + createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem; } export const ICommandManager = Symbol('ICommandManager'); @@ -445,8 +505,10 @@ export interface ICommandManager { */ registerCommand( command: E, + // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (...args: U) => any, - thisArg?: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + thisArg?: any, ): Disposable; /** @@ -465,8 +527,10 @@ export interface ICommandManager { */ registerTextEditorCommand( command: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, - thisArg?: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + thisArg?: any, ): Disposable; /** @@ -498,6 +562,16 @@ export interface ICommandManager { getCommands(filterInternal?: boolean): Thenable; } +export const IContextKeyManager = Symbol('IContextKeyManager'); +export interface IContextKeyManager { + setContext(key: ExtensionContextKey, value: boolean): Promise; +} + +export const IJupyterExtensionDependencyManager = Symbol('IJupyterExtensionDependencyManager'); +export interface IJupyterExtensionDependencyManager { + readonly isJupyterExtensionInstalled: boolean; +} + export const IDocumentManager = Symbol('IDocumentManager'); export interface IDocumentManager { @@ -517,7 +591,7 @@ export interface IDocumentManager { /** * The currently visible editors or an empty array. */ - readonly visibleTextEditors: TextEditor[]; + readonly visibleTextEditors: readonly TextEditor[]; /** * An [event](#Event) which fires when the [active editor](#window.activeTextEditor) @@ -537,7 +611,7 @@ export interface IDocumentManager { * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) * has changed. */ - readonly onDidChangeVisibleTextEditors: Event; + readonly onDidChangeVisibleTextEditors: Event; /** * An [event](#Event) which fires when the selection in an editor has changed. @@ -670,6 +744,16 @@ export interface IWorkspaceService { */ readonly rootPath: string | undefined; + /** + * When true, the user has explicitly trusted the contents of the workspace. + */ + readonly isTrusted: boolean; + + /** + * Event that fires when the current workspace has been trusted. + */ + readonly onDidGrantWorkspaceTrust: Event; + /** * List of workspace folders or `undefined` when no folder is open. * *Note* that the first entry corresponds to the value of `rootPath`. @@ -719,12 +803,9 @@ export interface IWorkspaceService { */ readonly onDidChangeConfiguration: Event; /** - * Whether a workspace folder exists - * @type {boolean} - * @memberof IWorkspaceService + * Returns if we're running in a virtual workspace. */ - readonly hasWorkspaceFolders: boolean; - + readonly isVirtualWorkspace: boolean; /** * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. * * returns `undefined` when the given uri doesn't match any workspace folder @@ -737,9 +818,6 @@ export interface IWorkspaceService { /** * Generate a key that's unique to the workspace folder (could be fsPath). - * @param {(Uri | undefined)} resource - * @returns {string} - * @memberof IWorkspaceService */ getWorkspaceFolderIdentifier(resource: Uri | undefined, defaultValue?: string): string; /** @@ -775,7 +853,7 @@ export interface IWorkspaceService { globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, - ignoreDeleteEvents?: boolean + ignoreDeleteEvents?: boolean, ): FileSystemWatcher; /** @@ -786,7 +864,8 @@ export interface IWorkspaceService { * will be matched against the file paths of resulting matches relative to their workspace. Use a [relative pattern](#RelativePattern) * to restrict the search results to a [workspace folder](#WorkspaceFolder). * @param exclude A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. + * will be matched against the file paths of resulting matches relative to their workspace. If `undefined` is passed, + * the glob patterns excluded in the `search.exclude` setting will be applied. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. Will return no results if no @@ -796,7 +875,7 @@ export interface IWorkspaceService { include: GlobPattern, exclude?: GlobPattern, maxResults?: number, - token?: CancellationToken + token?: CancellationToken, ): Thenable; /** @@ -810,9 +889,30 @@ export interface IWorkspaceService { * * @param section A dot-separated identifier. * @param resource A resource for which the configuration is asked for + * @param languageSpecific Should the [python] language-specific settings be obtained? * @return The full configuration or a subset. */ - getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration; + getConfiguration(section?: string, resource?: Uri, languageSpecific?: boolean): WorkspaceConfiguration; + + /** + * Opens an untitled text document. The editor will prompt the user for a file + * path when the document is to be saved. The `options` parameter allows to + * specify the *language* and/or the *content* of the document. + * + * @param options Options to control how the document will be created. + * @return A promise that resolves to a {@link TextDocument document}. + */ + openTextDocument(options?: { language?: string; content?: string }): Thenable; + /** + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful. + * + * **Note** that an editor with the provided resource must be opened in order to be saved. + * + * @param uri the associated uri for the opened editor to save. + * @return A thenable that resolves when the save operation has finished. + */ + save(uri: Uri): Thenable; } export const ITerminalManager = Symbol('ITerminalManager'); @@ -835,6 +935,12 @@ export interface ITerminalManager { * @return A new Terminal. */ createTerminal(options: TerminalOptions): Terminal; + + onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable; + + onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable; + + onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable; } export const IDebugService = Symbol('IDebugManager'); @@ -855,7 +961,7 @@ export interface IDebugService { /** * List of breakpoints. */ - readonly breakpoints: Breakpoint[]; + readonly breakpoints: readonly Breakpoint[]; /** * An [event](#Event) which fires when the [active debug session](#debug.activeDebugSession) @@ -927,7 +1033,7 @@ export interface IDebugService { startDebugging( folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, - parentSession?: DebugSession + parentSession?: DebugSession | DebugSessionOptions, ): Thenable; /** @@ -993,6 +1099,7 @@ export interface IApplicationEnvironment { * @type {any} * @memberof IApplicationEnvironment */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly packageJson: any; /** * Gets the full path to the user settings file. (may or may not exist). @@ -1009,6 +1116,10 @@ export interface IApplicationEnvironment { * @memberof IApplicationShell */ readonly shell: string; + /** + * An {@link Event} which fires when the default shell changes. + */ + readonly onDidChangeShell: Event; /** * Gets the vscode channel (whether 'insiders' or 'stable'). */ @@ -1028,131 +1139,22 @@ export interface IApplicationEnvironment { * The custom uri scheme the editor registers to in the operating system. */ readonly uriScheme: string; -} - -export const IWebPanelMessageListener = Symbol('IWebPanelMessageListener'); -export interface IWebPanelMessageListener extends IAsyncDisposable { /** - * Listens to web panel messages - * @param message: the message being sent - * @param payload: extra data that came with the message - * @return A IWebPanel that can be used to show html pages. + * The UI kind property indicates from which UI extensions + * are accessed from. For example, extensions could be accessed + * from a desktop application or a web browser. */ - onMessage(message: string, payload: any): void; + readonly uiKind: UIKind; /** - * Listens to web panel state changes - */ - onChangeViewState(panel: IWebPanel): void; -} - -export type WebPanelMessage = { - /** - * Message type - */ - type: string; - - /** - * Payload - */ - payload?: any; -}; - -// Wraps the VS Code webview panel -export const IWebPanel = Symbol('IWebPanel'); -export interface IWebPanel { - /** - * Event is fired when the load for a web panel fails - */ - readonly loadFailed: Event; - /** - * Convert a uri for the local file system to one that can be used inside webviews. + * The name of a remote. Defined by extensions, popular samples are `wsl` for the Windows + * Subsystem for Linux or `ssh-remote` for remotes using a secure shell. * - * 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): Uri; - setTitle(val: string): void; - /** - * Makes the webpanel show up. - * @return A Promise that can be waited on - */ - show(preserveFocus: boolean): Promise; - - /** - * Indicates if this web panel is visible or not. - */ - isVisible(): boolean; - - /** - * Sends a message to the hosted html page + * *Note* that the value is `undefined` when there is no remote extension host but that the + * value is defined in all extension hosts (local and remote) in case a remote extension host + * exists. Use {@link Extension.extensionKind} to know if + * a specific extension runs remote or not. */ - postMessage(message: WebPanelMessage): void; - - /** - * Attempts to close the panel if it's visible - */ - close(): void; - /** - * Indicates if the webview has the focus or not. - */ - isActive(): boolean; - /** - * Updates the current working directory for serving up files. - * @param cwd - */ - updateCwd(cwd: string): void; -} - -export interface IWebPanelOptions { - viewColumn: ViewColumn; - listener: IWebPanelMessageListener; - title: string; - rootPath: string; - /** - * Additional paths apart from cwd and rootPath, that webview would allow loading resources/files from. - * E.g. required for webview to serve images from worksapces when nb is in a nested folder. - */ - additionalPaths?: string[]; - scripts: string[]; - cwd: string; - // tslint:disable-next-line: no-any - settings?: any; - // Web panel to use if supplied by VS code instead - webViewPanel?: WebviewPanel; -} - -// Wraps the VS Code api for creating a web panel -export const IWebPanelProvider = Symbol('IWebPanelProvider'); -export interface IWebPanelProvider { - /** - * Creates a new webpanel - * - * @param {IWebPanelOptions} options - params for creating an IWebPanel - * @returns {IWebPanel} - * @memberof IWebPanelProvider - */ - create(options: IWebPanelOptions): Promise; -} - -// Wraps the vsls liveshare API -export const ILiveShareApi = Symbol('ILiveShareApi'); -export interface ILiveShareApi { - getApi(): Promise; -} - -// Wraps the liveshare api for testing -export const ILiveShareTestingApi = Symbol('ILiveShareTestingApi'); -export interface ILiveShareTestingApi extends ILiveShareApi { - isSessionStarted: boolean; - forceRole(role: vsls.Role): void; - startSession(): Promise; - stopSession(): Promise; - disableGuestChecker(): void; + readonly remoteName: string | undefined; } export const ILanguageService = Symbol('ILanguageService'); @@ -1178,8 +1180,6 @@ export interface ILanguageService { ): Disposable; } -export type Channel = 'stable' | 'insiders'; - /** * Wraps the `ActiveResourceService` API class. Created for injecting and mocking class methods in testing */ @@ -1188,326 +1188,6 @@ export interface IActiveResourceService { getActiveResource(): Resource; } -// Temporary hack to get the nyc compiler to find these types. vscode.proposed.d.ts doesn't work for some reason. - -// tslint:disable: interface-name -//#region Custom editor https://github.com/microsoft/vscode/issues/77131 - -/** - * Represents a custom document used by a [`CustomEditorProvider`](#CustomEditorProvider). - * - * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a `CustomDocument` is - * managed by VS Code. When no more references remain to a `CustomDocument`, it is disposed of. - */ -export interface CustomDocument { - /** - * The associated uri for this document. - */ - readonly uri: Uri; - - /** - * Dispose of the custom document. - * - * This is invoked by VS Code when there are no more references to a given `CustomDocument` (for example when - * all editors associated with the document have been closed.) - */ - dispose(): void; -} - -/** - * Event triggered by extensions to signal to VS Code that an edit has occurred on an [`CustomDocument`](#CustomDocument). - * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). - */ -export interface CustomDocumentEditEvent { - /** - * The document that the edit is for. - */ - readonly document: T; - - /** - * Display name describing the edit. - * - * This is shown in the UI to users. - */ - readonly label?: string; - - /** - * 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; - - /** - * 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; -} - -/** - * Event triggered by extensions to signal to VS Code that the content of a [`CustomDocument`](#CustomDocument) - * has changed. - * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). - */ -export interface CustomDocumentContentChangeEvent { - /** - * The document that the change is for. - */ - readonly document: T; -} - -/** - * A backup for an [`CustomDocument`](#CustomDocument). - */ -export interface CustomDocumentBackup { - /** - * Unique identifier for the backup. - * - * This id is passed back to your extension in `openCustomDocument` when opening a custom 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; -} - -/** - * Additional information used to implement [`CustomEditableDocument.backup`](#CustomEditableDocument.backup). - */ -export interface CustomDocumentBackupContext { - /** - * Suggested file location to write the new backup. - * - * Note that your extension is free to ignore this and use its own strategy for backup. - * - * For editors for workspace resource, this destination will be in the workspace storage. The path may not - */ - readonly destination: Uri; -} - -/** - * Additional information about the opening custom document. - */ -export interface CustomDocumentOpenContext { - /** - * The id of the backup to restore the document from or `undefined` if there is no backup. - * - * If this is provided, your extension should restore the editor from the backup instead of reading the file - * the user's workspace. - */ - readonly backupId?: string; -} - -/** - * Provider for readonly custom editors that use a custom document model. - * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). - * - * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. - * - * @param T Type of the custom document returned by this provider. - */ -export interface CustomReadonlyEditorProvider { - /** - * Create a new document for a given resource. - * - * `openCustomDocument` is called when the first editor for a given resource is opened, and the resolve document - * is passed to `resolveCustomEditor`. The resolved `CustomDocument` is re-used for subsequent editor opens. - * If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at - * this point will trigger another call to `openCustomDocument`. - * - * @param uri Uri of the document to open. - * @param openContext Additional information about the opening custom document. - * @param token A cancellation token that indicates the result is no longer needed. - * - * @return The custom document. - */ - openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): Thenable | T; - - /** - * Resolve a custom editor for a given resource. - * - * This is called whenever the user opens a new editor for this `CustomEditorProvider`. - * - * To resolve a custom editor, the provider must fill in its initial html content and hook up all - * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, - * for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. - * - * @param document Document for the resource being resolved. - * @param webviewPanel Webview to resolve. - * @param token A cancellation token that indicates the result is no longer needed. - * - * @return Optional thenable indicating that the custom editor has been resolved. - */ - resolveCustomEditor(document: T, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void; -} - -/** - * Provider for editiable custom editors that use a custom document model. - * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). - * This gives extensions full control over actions such as edit, save, and backup. - * - * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. - * - * @param T Type of the custom document returned by this provider. - */ -export interface CustomEditorProvider - extends CustomReadonlyEditorProvider { - /** - * Signal that an edit has occurred inside a custom editor. - * - * This event must be fired by your extension whenever an edit happens in a custom editor. An edit can be - * anything from changing some text, to cropping an image, to reordering a list. Your extension is free to - * define what an edit is and what data is stored on each edit. - * - * Firing `onDidChange` causes VS Code to mark the editors as being dirty. This is cleared when the user either - * saves or reverts the file. - * - * Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows - * users to undo and redo the edit using VS Code's standard VS Code keyboard shortcuts. VS Code will also mark - * the editor as no longer being dirty if the user undoes all edits to the last saved state. - * - * Editors that support editing but cannot use VS Code's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`. - * The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either - * `save` or `revert` the file. - * - * An editor should only ever fire `CustomDocumentEditEvent` events, or only ever fire `CustomDocumentContentChangeEvent` events. - */ - readonly onDidChangeCustomDocument: Event> | Event>; - - /** - * Save a custom document. - * - * This method is invoked by VS Code when the user saves a custom editor. This can happen when the user - * triggers save while the custom editor is active, by commands such as `save all`, or by auto save if enabled. - * - * To implement `save`, the implementer must persist the custom editor. This usually means writing the - * file data for the custom document to disk. After `save` completes, any associated editor instances will - * no longer be marked as dirty. - * - * @param document Document to save. - * @param cancellation Token that signals the save is no longer required (for example, if another save was triggered). - * - * @return Thenable signaling that saving has completed. - */ - saveCustomDocument(document: T, cancellation: CancellationToken): Thenable; - - /** - * Save a custom document to a different location. - * - * This method is invoked by VS Code when the user triggers 'save as' on a custom editor. The implementer must - * persist the custom editor to `destination`. - * - * When the user accepts save as, the current editor is be replaced by an non-dirty editor for the newly saved file. - * - * @param document Document to save. - * @param destination Location to save to. - * @param cancellation Token that signals the save is no longer required. - * - * @return Thenable signaling that saving has completed. - */ - saveCustomDocumentAs(document: T, destination: Uri, cancellation: CancellationToken): Thenable; - - /** - * Revert a custom document to its last saved state. - * - * This method is invoked by VS Code when the user triggers `File: Revert File` in a custom editor. (Note that - * this is only used using VS Code's `File: Revert File` command and not on a `git revert` of the file). - * - * To implement `revert`, the implementer must make sure all editor instances (webviews) for `document` - * are displaying the document in the same state is saved in. This usually means reloading the file from the - * workspace. - * - * @param document Document to revert. - * @param cancellation Token that signals the revert is no longer required. - * - * @return Thenable signaling that the change has completed. - */ - revertCustomDocument(document: T, cancellation: CancellationToken): Thenable; - - /** - * Back up a dirty custom document. - * - * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in - * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in - * the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource, - * your extension should first check to see if any backups exist for the resource. If there is a backup, your - * extension should load the file contents from there instead of from the resource in the workspace. - * - * `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are - * made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when - * `auto save` is enabled (since auto save already persists resource ). - * - * @param document Document to backup. - * @param context Information that can be used to backup the document. - * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your - * extension to decided how to respond to cancellation. If for example your extension is backing up a large file - * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather - * than cancelling it to ensure that VS Code has some valid backup. - */ - backupCustomDocument( - document: T, - context: CustomDocumentBackupContext, - cancellation: CancellationToken - ): Thenable; -} - -// #endregion - -export const ICustomEditorService = Symbol('ICustomEditorService'); -export interface ICustomEditorService { - /** - * Register a new provider for webview editors of a given type. - * - * @param viewType Type of the webview editor provider. - * @param provider Resolves webview editors. - * @param options Content settings for a webview panels the provider is given. - * - * @return Disposable that unregisters the `WebviewCustomEditorProvider`. - */ - registerCustomEditorProvider( - viewType: string, - provider: CustomReadonlyEditorProvider | 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; - /** - * Opens a file with a custom editor - */ - openEditor(file: Uri, viewType: string): Promise; -} export const IClipboard = Symbol('IClipboard'); export interface IClipboard { /** @@ -1520,30 +1200,3 @@ export interface IClipboard { */ writeText(value: string): Promise; } - -export type NotebookCellsChangeEvent = { type: 'changeCells' } & VSCNotebookCellsChangeEvent; -export type NotebookCellOutputsChangeEvent = { type: 'changeCellOutputs' } & VSCNotebookCellOutputsChangeEvent; -export type NotebookCellLanguageChangeEvent = { type: 'changeCellLanguage' } & VSCNotebookCellLanguageChangeEvent; -export type NotebookCellChangedEvent = - | NotebookCellsChangeEvent - | NotebookCellOutputsChangeEvent - | NotebookCellLanguageChangeEvent; -export const IVSCodeNotebook = Symbol('IVSCodeNotebook'); -export interface IVSCodeNotebook { - readonly notebookDocuments: ReadonlyArray; - readonly onDidOpenNotebookDocument: Event; - readonly onDidCloseNotebookDocument: Event; - readonly onDidChangeActiveNotebookEditor: Event; - readonly onDidChangeNotebookDocument: Event; - readonly notebookEditors: Readonly; - readonly activeNotebookEditor: NotebookEditor | undefined; - registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider): Disposable; - - registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable; - - registerNotebookOutputRenderer( - id: string, - outputSelector: NotebookOutputSelector, - renderer: NotebookOutputRenderer - ): Disposable; -} diff --git a/src/client/common/application/walkThroughs.ts b/src/client/common/application/walkThroughs.ts new file mode 100644 index 000000000000..89e57ee74e47 --- /dev/null +++ b/src/client/common/application/walkThroughs.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export enum PythonWelcome { + name = 'pythonWelcome', + windowsInstallId = 'python.installPythonWin8', + linuxInstallId = 'python.installPythonLinux', + macOSInstallId = 'python.installPythonMac', +} diff --git a/src/client/common/application/webPanels/webPanel.ts b/src/client/common/application/webPanels/webPanel.ts deleted file mode 100644 index 04c61e5583c1..000000000000 --- a/src/client/common/application/webPanels/webPanel.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../extensions'; - -import * as path from 'path'; -import { Event, EventEmitter, Uri, Webview, WebviewOptions, WebviewPanel, window } from 'vscode'; -import { Identifiers } from '../../../datascience/constants'; -import { traceError } from '../../logger'; -import { IFileSystem } from '../../platform/types'; -import { IDisposableRegistry } from '../../types'; -import * as localize from '../../utils/localize'; -import { IWebPanel, IWebPanelOptions, WebPanelMessage } from '../types'; - -export class WebPanel implements IWebPanel { - private panel: WebviewPanel | undefined; - private loadPromise: Promise; - private loadFailedEmitter = new EventEmitter(); - - constructor( - private fs: IFileSystem, - private disposableRegistry: IDisposableRegistry, - private options: IWebPanelOptions, - additionalRootPaths: Uri[] = [] - ) { - const webViewOptions: WebviewOptions = { - enableScripts: true, - localResourceRoots: [Uri.file(this.options.rootPath), Uri.file(this.options.cwd), ...additionalRootPaths] - }; - 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.loadPromise = this.load(); - } - - public get loadFailed(): Event { - return this.loadFailedEmitter.event; - } - public async show(preserveFocus: boolean) { - await this.loadPromise; - if (this.panel) { - this.panel.reveal(this.panel.viewColumn, preserveFocus); - } - } - - public updateCwd(_cwd: string) { - // See issue https://github.com/microsoft/vscode-python/issues/8933 for implementing this. - } - - public close() { - if (this.panel) { - this.panel.dispose(); - } - } - public asWebviewUri(localResource: Uri) { - if (!this.panel) { - throw new Error('WebView not initialized, too early to get a Uri'); - } - return this.panel?.webview.asWebviewUri(localResource); - } - - public isVisible(): boolean { - return this.panel ? this.panel.visible : false; - } - - public isActive(): boolean { - return this.panel ? this.panel.active : false; - } - - public postMessage(message: WebPanelMessage) { - if (this.panel && this.panel.webview) { - this.panel.webview.postMessage(message); - } - } - - public setTitle(newTitle: string) { - this.options.title = newTitle; - if (this.panel) { - this.panel.title = newTitle; - } - } - - // tslint:disable-next-line:no-any - private async load() { - try { - if (this.panel) { - const localFilesExist = await Promise.all(this.options.scripts.map((s) => this.fs.fileExists(s))); - if (localFilesExist.every((exists) => exists === true)) { - // Call our special function that sticks this script inside of an html page - // and translates all of the paths to vscode-resource URIs - this.panel.webview.html = await this.generateLocalReactHtml(this.panel.webview); - - // Reset when the current panel is closed - this.disposableRegistry.push( - this.panel.onDidDispose(() => { - this.panel = undefined; - this.options.listener.dispose().ignoreErrors(); - }) - ); - - this.disposableRegistry.push( - this.panel.webview.onDidReceiveMessage((message) => { - // Pass the message onto our listener - this.options.listener.onMessage(message.type, message.payload); - }) - ); - - this.disposableRegistry.push( - this.panel.onDidChangeViewState((_e) => { - // Pass the state change onto our listener - this.options.listener.onChangeViewState(this); - }) - ); - - // Set initial state - this.options.listener.onChangeViewState(this); - } else { - // Indicate that we can't load the file path - const badPanelString = localize.DataScience.badWebPanelFormatString(); - this.panel.webview.html = badPanelString.format(this.options.scripts.join(', ')); - } - } - } catch (error) { - // If our web panel failes to load, report that out so whatever - // is hosting the panel can clean up - traceError(`Error Loading WebPanel: ${error}`); - this.loadFailedEmitter.fire(); - } - } - - // tslint:disable-next-line:no-any - private async generateLocalReactHtml(webView: Webview) { - const uriBase = webView.asWebviewUri(Uri.file(this.options.cwd)).toString(); - const uris = this.options.scripts.map((script) => webView.asWebviewUri(Uri.file(script))); - const testFiles = await this.fs.getFiles(this.options.rootPath); - - // This method must be called so VSC is aware of files that can be pulled. - // Allow js and js.map files to be loaded by webpack in the webview. - testFiles - .filter((f) => f.toLowerCase().endsWith('.js') || f.toLowerCase().endsWith('.js.map')) - .forEach((f) => webView.asWebviewUri(Uri.file(f))); - - const rootPath = webView.asWebviewUri(Uri.file(this.options.rootPath)).toString(); - const fontAwesomePath = webView - .asWebviewUri( - Uri.file( - path.join(this.options.rootPath, 'node_modules', 'font-awesome', 'css', 'font-awesome.min.css') - ) - ) - .toString(); - return ` - - - - - - - - VS Code Python React UI - - - - - -
- - ${uris.map((uri) => ``).join('\n')} - - `; - } -} diff --git a/src/client/common/application/webPanels/webPanelProvider.ts b/src/client/common/application/webPanels/webPanelProvider.ts deleted file mode 100644 index 83a81a7eaf0d..000000000000 --- a/src/client/common/application/webPanels/webPanelProvider.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 '../../platform/types'; -import { IDisposableRegistry, IExtensionContext } from '../../types'; -import { IWebPanel, IWebPanelOptions, IWebPanelProvider } from '../types'; -import { WebPanel } from './webPanel'; - -@injectable() -export class WebPanelProvider implements IWebPanelProvider { - constructor( - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IExtensionContext) private readonly context: IExtensionContext - ) {} - - // tslint:disable-next-line:no-any - public async create(options: IWebPanelOptions): Promise { - // Allow loading resources from the `/tmp` folder when in webiviews. - // Used by widgets to place files that are not otherwise accessible. - const additionalRootPaths = [Uri.file(path.join(this.context.extensionPath, 'tmp'))]; - if (Array.isArray(options.additionalPaths)) { - additionalRootPaths.push(...options.additionalPaths.map((item) => Uri.file(item))); - } - return new WebPanel(this.fs, this.disposableRegistry, options, additionalRootPaths); - } -} diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index d004ff378364..a76a78777bef 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -9,11 +9,12 @@ import { Event, FileSystemWatcher, GlobPattern, + TextDocument, Uri, workspace, WorkspaceConfiguration, WorkspaceFolder, - WorkspaceFoldersChangeEvent + WorkspaceFoldersChangeEvent, } from 'vscode'; import { Resource } from '../types'; import { getOSType, OSType } from '../utils/platform'; @@ -35,14 +36,19 @@ export class WorkspaceService implements IWorkspaceService { 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 getConfiguration( + section?: string, + resource?: Uri, + languageSpecific: boolean = false, + ): WorkspaceConfiguration { + if (languageSpecific) { + return workspace.getConfiguration(section, { uri: resource, languageId: 'python' }); + } else { + return workspace.getConfiguration(section, resource); + } } public getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { return uri ? workspace.getWorkspaceFolder(uri) : undefined; @@ -52,31 +58,68 @@ export class WorkspaceService implements IWorkspaceService { } public createFileSystemWatcher( globPattern: GlobPattern, - _ignoreCreateEvents?: boolean, + ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, - ignoreDeleteEvents?: boolean + ignoreDeleteEvents?: boolean, ): FileSystemWatcher { return workspace.createFileSystemWatcher( globPattern, + ignoreCreateEvents, ignoreChangeEvents, - ignoreChangeEvents, - ignoreDeleteEvents + ignoreDeleteEvents, ); } public findFiles( include: GlobPattern, exclude?: GlobPattern, maxResults?: number, - token?: CancellationToken + token?: CancellationToken, ): Thenable { - return workspace.findFiles(include, exclude, maxResults, token); + const excludePattern = exclude === undefined ? this.searchExcludes : exclude; + return workspace.findFiles(include, excludePattern, 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 + getOSType() === OSType.Windows + ? workspaceFolder.uri.fsPath.toUpperCase() + : workspaceFolder.uri.fsPath, ) : defaultValue; } + + public get isVirtualWorkspace(): boolean { + const isVirtualWorkspace = + workspace.workspaceFolders && workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file'); + return !!isVirtualWorkspace; + } + + public get isTrusted(): boolean { + return workspace.isTrusted; + } + + public get onDidGrantWorkspaceTrust(): Event { + return workspace.onDidGrantWorkspaceTrust; + } + + public openTextDocument(options?: { language?: string; content?: string }): Thenable { + return workspace.openTextDocument(options); + } + + private get searchExcludes() { + const searchExcludes = this.getConfiguration('search.exclude'); + const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true); + return `{${enabledSearchExcludes.join(',')}}`; + } + + public async save(uri: Uri): Promise { + try { + // This is a proposed API hence putting it inside try...catch. + const result = await workspace.save(uri); + return result; + } catch (ex) { + return undefined; + } + } } diff --git a/src/client/common/asyncDisposableRegistry.ts b/src/client/common/asyncDisposableRegistry.ts deleted file mode 100644 index 10bd9492b31e..000000000000 --- a/src/client/common/asyncDisposableRegistry.ts +++ /dev/null @@ -1,27 +0,0 @@ -// 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 index 601ac933979d..b24abc7ab493 100644 --- a/src/client/common/cancellation.ts +++ b/src/client/common/cancellation.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; -import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { CancellationToken, CancellationTokenSource, CancellationError as VSCCancellationError } from 'vscode'; import { createDeferred } from './utils/async'; import * as localize from './utils/localize'; @@ -11,16 +11,15 @@ import * as localize from './utils/localize'; */ export class CancellationError extends Error { constructor() { - super(localize.Common.canceled()); + super(localize.Common.canceled); + } + + static isCancellationError(error: unknown): error is CancellationError { + return error instanceof CancellationError || error instanceof VSCCancellationError; } } /** * 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; @@ -33,7 +32,8 @@ export function createPromiseFromCancellation(options: { return; } const complete = () => { - if (options.token!.isCancellationRequested) { + const optionsToken = options.token!; // NOSONAR + if (optionsToken.isCancellationRequested) { if (options.cancelAction === 'resolve') { return resolve(options.defaultValue); } @@ -49,10 +49,6 @@ export function createPromiseFromCancellation(options: { /** * 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(); @@ -116,7 +112,6 @@ export namespace Cancellation { /** * 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; @@ -124,7 +119,6 @@ export namespace Cancellation { /** * throws a CancellationError if the token is canceled. - * @param cancelToken */ export function throwIfCanceled(cancelToken?: CancellationToken): void { if (isCanceled(cancelToken)) { diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index d0066f58b378..91c06d9331fd 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,64 +1,58 @@ 'use strict'; -import * as child_process from 'child_process'; +// eslint-disable-next-line camelcase import * as path from 'path'; +import * as fs from 'fs'; import { ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, Event, EventEmitter, Uri, - WorkspaceConfiguration + WorkspaceConfiguration, } from 'vscode'; import { LanguageServerType } from '../activation/types'; -import '../common/extensions'; -import { IInterpreterAutoSeletionProxyService, IInterpreterSecurityService } from '../interpreter/autoSelection/types'; -import { LogLevel } from '../logging/levels'; +import './extensions'; +import { IInterpreterAutoSelectionProxyService } from '../interpreter/autoSelection/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { sendSettingTelemetry } from '../telemetry/envFileTelemetry'; +import { ITestingSettings } from '../testing/configuration/types'; 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 { DEFAULT_INTERPRETER_SETTING, isTestExecution, PYREFLY_EXTENSION_ID } from './constants'; import { - IAnalysisSettings, IAutoCompleteSettings, - IDataScienceSettings, + IDefaultLanguageServer, IExperiments, - IExperimentsManager, - IFormattingSettings, + IExtensions, IInterpreterPathService, - ILintingSettings, - ILoggingSettings, + IInterpreterSettings, IPythonSettings, - ISortImportSettings, + IREPLSettings, ITerminalSettings, - ITestingSettings, - IWorkspaceSymbolSettings, - LoggingLevelSettingType, - Resource + Resource, } from './types'; import { debounceSync } from './utils/decorators'; import { SystemVariables } from './variables/systemVariables'; +import { getOSType, OSType, isWindows } from './utils/platform'; +import { untildify } from './helpers'; -// tslint:disable:no-require-imports no-var-requires -const untildify = require('untildify'); - -// tslint:disable-next-line:completed-docs export class PythonSettings implements IPythonSettings { - public get onDidChange(): Event { + private get onDidChange(): Event { return this.changed.event; } + // eslint-disable-next-line class-methods-use-this + public static onConfigChange(): Event { + return PythonSettings.configChanged.event; + } + public get pythonPath(): string { return this._pythonPath; } + public set pythonPath(value: string) { if (this._pythonPath === value) { return; @@ -75,6 +69,7 @@ export class PythonSettings implements IPythonSettings { public get defaultInterpreterPath(): string { return this._defaultInterpreterPath; } + public set defaultInterpreterPath(value: string) { if (this._defaultInterpreterPath === value) { return; @@ -87,63 +82,79 @@ export class PythonSettings implements IPythonSettings { 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 interpreter!: IInterpreterSettings; + public venvFolders: string[] = []; + + public activeStateToolPath = ''; + public condaPath = ''; + public pipenvPath = ''; + public poetryPath = ''; + + public pixiToolPath = ''; + 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 REPL!: IREPLSettings; + public experiments!: IExperiments; - public languageServer: LanguageServerType = LanguageServerType.Microsoft; - public logging: ILoggingSettings = { level: LogLevel.Error }; - protected readonly changed = new EventEmitter(); + public languageServer: LanguageServerType = LanguageServerType.Node; + + public languageServerIsDefault = true; + + protected readonly changed = new EventEmitter(); + + private static readonly configChanged = new EventEmitter(); + private workspaceRoot: Resource; + private disposables: Disposable[] = []; - // tslint:disable-next-line:variable-name - private _pythonPath = ''; + + private _pythonPath = 'python'; + 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 + private readonly interpreterAutoSelectionService: IInterpreterAutoSelectionProxyService, + workspace: IWorkspaceService, + private readonly interpreterPathService: IInterpreterPathService, + private readonly defaultLS: IDefaultLanguageServer | undefined, + private readonly extensions: IExtensions, ) { 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 + interpreterAutoSelectionService: IInterpreterAutoSelectionProxyService, + workspace: IWorkspaceService, + interpreterPathService: IInterpreterPathService, + defaultLS: IDefaultLanguageServer | undefined, + extensions: IExtensions, ): PythonSettings { workspace = workspace || new WorkspaceService(); const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; @@ -154,28 +165,32 @@ export class PythonSettings implements IPythonSettings { workspaceFolderUri, interpreterAutoSelectionService, workspace, - experimentsManager, interpreterPathService, - interpreterSecurityService + defaultLS, + extensions, ); PythonSettings.pythonSettings.set(workspaceFolderKey, settings); + settings.onDidChange((event) => PythonSettings.debounceConfigChangeNotification(event)); // 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)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const config = workspace.getConfiguration('editor', 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 + @debounceSync(1) + // eslint-disable-next-line class-methods-use-this + protected static debounceConfigChangeNotification(event?: ConfigurationChangeEvent): void { + PythonSettings.configChanged.fire(event); + } + public static getSettingsUriAndTarget( resource: Uri | undefined, - workspace?: IWorkspaceService + workspace?: IWorkspaceService, ): { uri: Uri | undefined; target: ConfigurationTarget } { workspace = workspace || new WorkspaceService(); const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; @@ -189,282 +204,134 @@ export class PythonSettings implements IPythonSettings { return { uri: workspaceFolderUri, target }; } - // tslint:disable-next-line:function-name - public static dispose() { + public static dispose(): void { 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 + + public static toSerializable(settings: IPythonSettings): IPythonSettings { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const clone: any = {}; + const keys = Object.entries(settings); + keys.forEach((e) => { + const [k, v] = e; + if (!k.includes('Manager') && !k.includes('Service') && !k.includes('onDid')) { + clone[k] = v; + } + }); + + return clone as IPythonSettings; + } + + public dispose(): void { this.disposables.forEach((disposable) => disposable && disposable.dispose()); this.disposables = []; } - // tslint:disable-next-line:cyclomatic-complexity max-func-body-length - protected update(pythonSettings: WorkspaceConfiguration) { + + protected update(pythonSettings: WorkspaceConfiguration): void { const workspaceRoot = this.workspaceRoot?.fsPath; const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace); - this.pythonPath = this.getPythonPath(pythonSettings, systemVariables, workspaceRoot); + this.pythonPath = this.getPythonPath(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 = defaultInterpreterPath || DEFAULT_INTERPRETER_SETTING; + if (this.defaultInterpreterPath === DEFAULT_INTERPRETER_SETTING) { + const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter( + this.workspaceRoot, + ); + this.defaultInterpreterPath = autoSelectedPythonInterpreter?.path ?? this.defaultInterpreterPath; + } 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 activeStateToolPath = systemVariables.resolveAny(pythonSettings.get('activeStateToolPath'))!; + this.activeStateToolPath = + activeStateToolPath && activeStateToolPath.length > 0 + ? getAbsolutePath(activeStateToolPath, workspaceRoot) + : activeStateToolPath; 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; + const pixiToolPath = systemVariables.resolveAny(pythonSettings.get('pixiToolPath'))!; + this.pixiToolPath = + pixiToolPath && pixiToolPath.length > 0 ? getAbsolutePath(pixiToolPath, workspaceRoot) : pixiToolPath; - 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.interpreter = pythonSettings.get('interpreter') ?? { + infoVisibility: 'onPythonRelated', + }; + // Get as a string and verify; don't just accept. + let userLS = pythonSettings.get('languageServer'); + userLS = systemVariables.resolveAny(userLS); + + // Validate the user's input; if invalid, set it to the default. + if ( + !userLS || + userLS === 'Default' || + userLS === 'Microsoft' || + !Object.values(LanguageServerType).includes(userLS as LanguageServerType) + ) { + if ( + this.extensions.getExtension(PYREFLY_EXTENSION_ID) && + pythonSettings.get('pyrefly.disableLanguageServices') !== true + ) { + this.languageServer = LanguageServerType.None; + } else { + this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; + } + this.languageServerIsDefault = true; + } else if (userLS === 'JediLSP') { + // Switch JediLSP option to Jedi. + this.languageServer = LanguageServerType.Jedi; + this.languageServerIsDefault = false; + } else { + this.languageServer = userLS as LanguageServerType; + this.languageServerIsDefault = false; } - 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); + const autoCompleteSettings = systemVariables.resolveAny( + pythonSettings.get('autoComplete'), + )!; + if (this.autoComplete) { + Object.assign(this.autoComplete, autoCompleteSettings); } else { - this.jediPath = ''; + this.autoComplete = autoCompleteSettings; } - 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 + // eslint-disable-next-line @typescript-eslint/no-explicit-any this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - // 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 { - this.linting = lintingSettings; - } - - // 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 { - 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 - ); - - // 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 { - 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 + autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', } as ITestingSettings; } } @@ -475,39 +342,29 @@ export class PythonSettings implements IPythonSettings { : { promptToConfigure: true, debugPort: 3000, - nosetestArgs: [], - nosetestPath: 'nosetest', - nosetestsEnabled: false, pytestArgs: [], pytestEnabled: false, pytestPath: 'pytest', unittestArgs: [], unittestEnabled: false, - autoTestDiscoverOnSaveEnabled: true + autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', }; 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; } } @@ -516,42 +373,40 @@ export class PythonSettings implements IPythonSettings { ? this.terminal : { executeInFileDir: true, + focusAfterLaunch: false, launchArgs: [], activateEnvironment: true, - activateEnvInCurrentTerminal: false + activateEnvInCurrentTerminal: false, + shellIntegration: { + enabled: false, + }, }; - const experiments = systemVariables.resolveAny(pythonSettings.get('experiments'))!; + this.REPL = pythonSettings.get('REPL')!; + const experiments = pythonSettings.get('experiments')!; if (this.experiments) { Object.assign(this.experiments, experiments); } else { this.experiments = experiments; } + // Note we directly access experiment settings using workspace service in ExperimentService class. + // Any changes here specific to these settings should propogate their as well. this.experiments = this.experiments ? this.experiments : { enabled: true, optInto: [], - optOutFrom: [] + optOutFrom: [], }; - - const dataScienceSettings = systemVariables.resolveAny( - pythonSettings.get('dataScience') - )!; - if (this.datascience) { - Object.assign(this.datascience, dataScienceSettings); - } else { - this.datascience = dataScienceSettings; - } - - this.insidersChannel = pythonSettings.get('insidersChannel')!; } - protected getPythonExecutable(pythonPath: string) { + // eslint-disable-next-line class-methods-use-this + protected getPythonExecutable(pythonPath: string): string { return getPythonExecutable(pythonPath); } - protected onWorkspaceFoldersChanged() { - //If an activated workspace folder was removed, delete its key + + protected onWorkspaceFoldersChanged(): void { + // 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); @@ -561,31 +416,41 @@ export class PythonSettings implements IPythonSettings { } } } - 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(); - }; + + public register(): void { + PythonSettings.pythonSettings = new Map(); + this.initialize(); + } + + private onDidChanged(event?: ConfigurationChangeEvent) { + 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(event); + } + + public initialize(): void { this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); this.disposables.push( - this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)) + this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + this.onDidChanged(); + }), ); - 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(); + this.onDidChanged(event); } - }) + }), ); if (this.interpreterPathService) { - this.disposables.push(this.interpreterPathService.onDidChange(onDidChange.bind(this))); + this.disposables.push( + this.interpreterPathService.onDidChange(() => { + this.onDidChanged(); + }), + ); } const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot); @@ -593,59 +458,31 @@ export class PythonSettings implements IPythonSettings { this.update(initialConfig); } } + @debounceSync(1) - protected debounceChangeNotification() { - this.changed.fire(); + protected debounceChangeNotification(event?: ConfigurationChangeEvent): void { + this.changed.fire(event); } - 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')) { + private getPythonPath(systemVariables: SystemVariables, workspaceRoot: string | undefined) { + this.pythonPath = systemVariables.resolveAny(this.interpreterPathService.get(this.workspaceRoot))!; + if ( + !process.env.CI_DISABLE_AUTO_SELECTION && + (this.pythonPath.length === 0 || this.pythonPath === 'python') && + this.interpreterAutoSelectionService + ) { const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter( - this.workspaceRoot + 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; + if (autoSelectedPythonInterpreter) { + this.pythonPath = autoSelectedPythonInterpreter.path; + if (this.workspaceRoot) { 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); } } @@ -654,7 +491,7 @@ function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): stri if (!rootDir) { rootDir = __dirname; } - // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pathToCheck = untildify(pathToCheck) as string; if (isTestExecution() && !pathToCheck) { return rootDir; @@ -666,7 +503,6 @@ function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): stri } function getPythonExecutable(pythonPath: string): string { - // tslint:disable-next-line:prefer-type-cast no-unsafe-any pythonPath = untildify(pythonPath) as string; // If only 'python'. @@ -682,18 +518,29 @@ function getPythonExecutable(pythonPath: string): string { 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']; + + const KnownPythonExecutables = [ + 'python', + 'python4', + 'python3.6', + 'python3.5', + 'python3', + 'python2.7', + 'python2', + 'python3.7', + 'python3.8', + 'python3.9', + ]; for (let executableName of KnownPythonExecutables) { // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. - if (IS_WINDOWS) { + if (isWindows()) { 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); + if (isValidPythonPath(path.join(pythonPath, 'Scripts', executableName))) { + return path.join(pythonPath, 'Scripts', executableName); } } else { if (isValidPythonPath(path.join(pythonPath, executableName))) { @@ -709,31 +556,8 @@ function getPythonExecutable(pythonPath: string): string { } 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; - } - } + return ( + fs.existsSync(pythonPath) && + path.basename(getOSType() === OSType.Windows ? pythonPath.toLowerCase() : pythonPath).startsWith('python') + ); } diff --git a/src/client/common/configuration/executionSettings/pipEnvExecution.ts b/src/client/common/configuration/executionSettings/pipEnvExecution.ts new file mode 100644 index 000000000000..de1d90e6fc84 --- /dev/null +++ b/src/client/common/configuration/executionSettings/pipEnvExecution.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable, inject } from 'inversify'; +import { IConfigurationService, IToolExecutionPath } from '../../types'; + +@injectable() +export class PipEnvExecutionPath implements IToolExecutionPath { + constructor(@inject(IConfigurationService) private readonly configService: IConfigurationService) {} + + public get executable(): string { + return this.configService.getSettings().pipenvPath; + } +} diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index b922254d8b05..443990b2e5da 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -2,69 +2,69 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; -import { - IInterpreterAutoSeletionProxyService, - IInterpreterSecurityService -} from '../../interpreter/autoSelection/types'; +import { ConfigurationTarget, Event, Uri, WorkspaceConfiguration, ConfigurationChangeEvent } from 'vscode'; +import { IInterpreterAutoSelectionService } 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'; +import { + IConfigurationService, + IDefaultLanguageServer, + IExtensions, + 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); } + + // eslint-disable-next-line class-methods-use-this + public get onDidChange(): Event { + return PythonSettings.onConfigChange(); + } + public getSettings(resource?: Uri): IPythonSettings { - const InterpreterAutoSelectionService = this.serviceContainer.get( - IInterpreterAutoSeletionProxyService + const InterpreterAutoSelectionService = this.serviceContainer.get( + IInterpreterAutoSelectionService, ); const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); - const experiments = this.serviceContainer.get(IExperimentsManager); - const interpreterSecurityService = this.serviceContainer.get( - IInterpreterSecurityService - ); + const defaultLS = this.serviceContainer.tryGet(IDefaultLanguageServer); + const extensions = this.serviceContainer.get(IExtensions); return PythonSettings.getInstance( resource, InterpreterAutoSelectionService, this.workspaceService, - experiments, interpreterPathService, - interpreterSecurityService + defaultLS, + extensions, ); } public async updateSectionSetting( section: string, setting: string, - value?: {}, + value?: unknown, resource?: Uri, - configTarget?: ConfigurationTarget + 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 + target: configTarget || ConfigurationTarget.WorkspaceFolder, }; let settingsInfo = defaultSetting; if (section === 'python' && configTarget !== ConfigurationTarget.Global) { settingsInfo = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService); } - configTarget = configTarget ? configTarget : settingsInfo.target; + 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); + const currentValue = configSection.inspect(setting); if ( currentValue !== undefined && @@ -74,26 +74,20 @@ export class ConfigurationService implements IConfigurationService { ) { 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); - } + await configSection.update(setting, value, configTarget); + await this.verifySetting(configSection, configTarget, setting, value); } public async updateSetting( setting: string, - value?: {}, + value?: unknown, resource?: Uri, - configTarget?: ConfigurationTarget + configTarget?: ConfigurationTarget, ): Promise { return this.updateSectionSetting('python', setting, value, resource, configTarget); } + // eslint-disable-next-line class-methods-use-this public isTestExecution(): boolean { return process.env.VSC_PYTHON_CI_TEST === '1'; } @@ -102,7 +96,7 @@ export class ConfigurationService implements IConfigurationService { configSection: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, - value?: {} + value?: unknown, ): Promise { if (this.isTestExecution() && !isUnitTestExecution()) { let retries = 0; @@ -113,12 +107,14 @@ export class ConfigurationService implements IConfigurationService { } if (setting && value !== undefined) { // Both specified - const actual = - target === ConfigurationTarget.Global - ? setting.globalValue - : target === ConfigurationTarget.Workspace - ? setting.workspaceValue - : setting.workspaceFolderValue; + let actual; + if (target === ConfigurationTarget.Global) { + actual = setting.globalValue; + } else if (target === ConfigurationTarget.Workspace) { + actual = setting.workspaceValue; + } else { + actual = setting.workspaceFolderValue; + } if (actual === value) { break; } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 6096285fa170..15fd037a3d9f 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -1,100 +1,107 @@ +/* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/no-namespace */ 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 InteractiveInputScheme = 'vscode-interactive-input'; +export const InteractiveScheme = 'vscode-interactive'; export const PYTHON = [ { scheme: 'file', language: PYTHON_LANGUAGE }, { scheme: 'untitled', language: PYTHON_LANGUAGE }, { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, - { scheme: NotebookCellScheme, language: PYTHON_LANGUAGE } + { scheme: NotebookCellScheme, language: PYTHON_LANGUAGE }, + { scheme: InteractiveInputScheme, language: PYTHON_LANGUAGE }, +]; + +export const PYTHON_NOTEBOOKS = [ + { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, + { scheme: NotebookCellScheme, language: PYTHON_LANGUAGE }, + { scheme: InteractiveInputScheme, 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 const PYREFLY_EXTENSION_ID = 'meta.pyrefly'; +export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; +export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard'; +export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; + +export type Channel = 'stable' | 'insiders'; + +export enum CommandSource { + ui = 'ui', + commandPalette = 'commandpalette', +} export namespace Commands { - export const Set_Interpreter = 'python.setInterpreter'; - export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; + export const ClearStorage = 'python.clearCacheAndReload'; + export const CreateNewFile = 'python.createNewFile'; + export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; + export const Create_Environment = 'python.createEnvironment'; + export const CopyTestId = 'python.copyTestId'; + export const Create_Environment_Button = 'python.createEnvironment-button'; + export const Create_Environment_Check = 'python.createEnvironmentCheck'; + export const Create_Terminal = 'python.createTerminal'; + export const Debug_In_Terminal = 'python.debugInTerminal'; 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_In_Separate_Terminal = 'python.execInDedicatedTerminal'; + export const Exec_In_REPL = 'python.execInREPL'; 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 Exec_In_REPL_Enter = 'python.execInREPLEnter'; + export const Exec_In_IW_Enter = 'python.execInInteractiveWindowEnter'; + export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; + export const GetSelectedInterpreterPath = 'python.interpreterPath'; + export const InstallJupyter = 'python.installJupyter'; + export const InstallPython = 'python.installPython'; + export const InstallPythonOnLinux = 'python.installPythonOnLinux'; + export const InstallPythonOnMac = 'python.installPythonOnMac'; + export const PickLocalProcess = 'python.pickLocalProcess'; + export const ReportIssue = 'python.reportIssue'; + export const Set_Interpreter = 'python.setInterpreter'; + export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; + export const Start_REPL = 'python.startREPL'; + export const Start_Native_REPL = 'python.startNativeREPL'; 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 Tests_CopilotSetup = 'python.copilotSetupTests'; + export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; 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'; } + +// Look at https://microsoft.github.io/vscode-codicons/dist/codicon.html for other Octicon icon ids export namespace Octicons { + export const Add = '$(add)'; 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 Search = '$(search)'; + export const Search_Stop = '$(search-stop)'; + export const Star = '$(star-full)'; + export const Gear = '$(gear)'; + export const Warning = '$(warning)'; + export const Error = '$(error)'; + export const Lightbulb = '$(lightbulb)'; + export const Folder = '$(folder)'; } -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; +/** + * Look at https://code.visualstudio.com/api/references/icons-in-labels#icon-listing for ThemeIcon ids. + * Using a theme icon is preferred over a custom icon as it gives product theme authors the possibility + * to change the icons. + */ +export namespace ThemeIcons { + export const Refresh = 'refresh'; + export const SpinningLoader = 'loading~spin'; } 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 const isCI = + process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined || process.env.GITHUB_ACTIONS === 'true'; export function isTestExecution(): boolean { return process.env.VSC_PYTHON_CI_TEST === '1' || isUnitTestExecution(); @@ -102,17 +109,12 @@ export function isTestExecution(): boolean { /** * Whether we're running unit tests (*.unit.test.ts). - * These tests have a speacial meaning, they run fast. - * @export - * @returns {boolean} + * These tests have a special meaning, they run fast. */ 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/crypto.ts b/src/client/common/crypto.ts deleted file mode 100644 index 3067ebb4a801..000000000000 --- a/src/client/common/crypto.ts +++ /dev/null @@ -1,41 +0,0 @@ -// 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 deleted file mode 100644 index 421fd7f5dfa9..000000000000 --- a/src/client/common/dotnet/compatibilityService.ts +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 1c1f18bb5845..000000000000 --- a/src/client/common/dotnet/serviceRegistry.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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 deleted file mode 100644 index dbd67d221371..000000000000 --- a/src/client/common/dotnet/services/linuxCompatibilityService.ts +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index 0fbf44b0fa68..000000000000 --- a/src/client/common/dotnet/services/macCompatibilityService.ts +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index 728a29eacf37..000000000000 --- a/src/client/common/dotnet/services/unknownOsCompatibilityService.ts +++ /dev/null @@ -1,16 +0,0 @@ -// 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 deleted file mode 100644 index 6e616909de3c..000000000000 --- a/src/client/common/dotnet/services/windowsCompatibilityService.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index 75db7d6850c3..000000000000 --- a/src/client/common/dotnet/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 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 deleted file mode 100644 index 2e32e3a81642..000000000000 --- a/src/client/common/editor.ts +++ /dev/null @@ -1,409 +0,0 @@ -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 index 9364d3a971df..7867d5ccfe30 100644 --- a/src/client/common/errors/errorUtils.ts +++ b/src/client/common/errors/errorUtils.ts @@ -1,9 +1,6 @@ // 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 && @@ -15,13 +12,10 @@ export class ErrorUtils { } /** - * Wraps an error with a custom error message, retaining the call stack information. + * An error class that contains a telemetry safe reason. */ -export class WrappedError extends Error { - constructor(message: string, originalException: Error) { +export class ErrorWithTelemetrySafeReason extends Error { + constructor(message: string, public readonly telemetrySafeReason: string) { 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/experiments/groups.ts b/src/client/common/experiments/groups.ts index 6a01a3a7e46a..12f4ef89018b 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -1,98 +1,21 @@ -// 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 = 'pythonSurveyNotification', } -/* - * 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' +export enum ShowToolsExtensionPrompt { + experiment = 'pythonPromptNewToolsExt', } -// 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' +export enum TerminalEnvVarActivation { + experiment = 'pythonTerminalEnvVarActivation', } -// Experiment to turn on trusted notebooks checks -export enum EnableTrustedNotebooks { - experiment = 'EnableTrustedNotebooks' +export enum DiscoveryUsingWorkers { + experiment = 'pythonDiscoveryUsingWorkers', } -// Experiment to offer switch to Pylance language server -export enum TryPylance { - experiment = 'tryPylance' +// Experiment to enable the new testing rewrite. +export enum EnableTestAdapterRewrite { + experiment = 'pythonTestAdapter', } diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts new file mode 100644 index 000000000000..f6ae39d260f5 --- /dev/null +++ b/src/client/common/experiments/helpers.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { env, workspace } from 'vscode'; +import { IExperimentService } from '../types'; +import { TerminalEnvVarActivation } from './groups'; +import { isTestExecution } from '../constants'; +import { traceInfo } from '../../logging'; + +export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { + if (!isTestExecution() && env.remoteName && workspace.workspaceFolders && workspace.workspaceFolders.length > 1) { + // TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed. + traceInfo('Not enabling terminal env var experiment in multiroot remote workspaces'); + return false; + } + if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { + return false; + } + return true; +} diff --git a/src/client/common/experiments/manager.ts b/src/client/common/experiments/manager.ts deleted file mode 100644 index 4ad4673de419..000000000000 --- a/src/client/common/experiments/manager.ts +++ /dev/null @@ -1,364 +0,0 @@ -// 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/master/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; - } - 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 index 6d30c0915349..e52773004fb3 100644 --- a/src/client/common/experiments/service.ts +++ b/src/client/common/experiments/service.ts @@ -3,25 +3,19 @@ 'use strict'; -import { inject, injectable, named } from 'inversify'; -import { Memento } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { l10n } from 'vscode'; import { getExperimentationService, IExperimentationService, TargetPopulation } from 'vscode-tas-client'; +import { traceLog } from '../../logging'; 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 { IApplicationEnvironment, IWorkspaceService } from '../application/types'; +import { PVSC_EXTENSION_ID } from '../constants'; +import { IExperimentService, IPersistentStateFactory } from '../types'; import { ExperimentationTelemetry } from './telemetry'; const EXP_MEMENTO_KEY = 'VSCode.ABExp.FeatureData'; +const EXP_CONFIG_ID = 'vscode'; @injectable() export class ExperimentService implements IExperimentService { @@ -29,37 +23,48 @@ 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 experiments = this.persistentState.createGlobalPersistentState<{ features: string[] }>( + EXP_MEMENTO_KEY, + { features: [] }, + ); + + private readonly enabled: boolean; + private readonly experimentationService?: IExperimentationService; - private readonly settings: IPythonSettings; constructor( - @inject(IConfigurationService) readonly configurationService: IConfigurationService, + @inject(IWorkspaceService) readonly workspaceService: IWorkspaceService, @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 + @inject(IPersistentStateFactory) private readonly persistentState: IPersistentStateFactory, ) { - this.settings = configurationService.getSettings(undefined); - + const settings = this.workspaceService.getConfiguration('python'); // 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; + const optInto = settings.get('experiments.optInto') || []; + const optOutFrom = settings.get('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) { + // If users opt out of all experiments we treat it as disabling them. + // The `experiments.enabled` setting also needs to be explicitly disabled, default to true otherwise. + if (this._optOutFrom.includes('All') || settings.get('experiments.enabled') === false) { + this.enabled = false; + } else { + this.enabled = true; + } + + if (!this.enabled) { return; } let targetPopulation: TargetPopulation; - - if (this.appEnvironment.extensionChannel === 'insiders') { + // if running in VS Code Insiders, use the Insiders target population + if (this.appEnvironment.channel === 'insiders') { targetPopulation = TargetPopulation.Insiders; } else { targetPopulation = TargetPopulation.Public; @@ -72,46 +77,180 @@ export class ExperimentService implements IExperimentService { this.appEnvironment.packageJson.version!, targetPopulation, telemetryReporter, - this.globalState + this.experiments.storage, ); + } + + public async activate(): Promise { + if (this.experimentationService) { + const initStart = Date.now(); + await this.experimentationService.initializePromise; + + if (this.experiments.value.features.length === 0) { + // Only await on this if we don't have anything in cache. + // This means that we start the session with partial experiment info. + // We accept this as a compromise to avoid delaying startup. - this.logExperiments(); + // In the case where we don't wait on this promise. If the experiment info changes, + // those changes will be applied in the next session. This is controlled internally + // in the tas-client via `overrideInMemoryFeatures` value that is passed to + // `getFeaturesAsync`. At the time of writing this comment the value of + // `overrideInMemoryFeatures` was always passed in as `false`. So, the experiment + // states did not change mid way. + await this.experimentationService.initialFetch; + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_INIT_PERFORMANCE, Date.now() - initStart); + } + this.logExperiments(); + } + sendOptInOptOutTelemetry(this._optInto, this._optOutFrom, this.appEnvironment.packageJson); } public async inExperiment(experiment: string): Promise { + return this.inExperimentSync(experiment); + } + + public inExperimentSync(experiment: string): boolean { 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. + // Currently the service doesn't support opting in and out of experiments. + // so we need to perform these checks 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 - }); - + // Check if the user was already in the experiment server-side. We need to do + // this to ensure the experiment service is ready and internal states are fully + // synced with the experiment server. + this.experimentationService.getTreatmentVariable(EXP_CONFIG_ID, experiment); return true; } - return this.experimentationService.isCachedFlightEnabled(experiment); + // If getTreatmentVariable returns undefined, + // it means that the value for this experiment was not found on the server. + const treatmentVariable = this.experimentationService.getTreatmentVariable(EXP_CONFIG_ID, experiment); + + return treatmentVariable === true; + } + + public async getExperimentValue(experiment: string): Promise { + if (!this.experimentationService || this._optOutFrom.includes('All') || this._optOutFrom.includes(experiment)) { + return undefined; + } + + return this.experimentationService.getTreatmentVariable(EXP_CONFIG_ID, experiment); } private logExperiments() { - const experiments = this.globalState.get<{ features: string[] }>(EXP_MEMENTO_KEY, { features: [] }); + const telemetrySettings = this.workspaceService.getConfiguration('telemetry'); + let experimentsDisabled = false; + if (telemetrySettings && telemetrySettings.get('enableTelemetry') === false) { + traceLog('Telemetry is disabled'); + experimentsDisabled = true; + } - 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)); - } - }); + if (telemetrySettings && telemetrySettings.get('telemetryLevel') === 'off') { + traceLog('Telemetry level is off'); + experimentsDisabled = true; + } + + if (experimentsDisabled) { + traceLog('Experiments are disabled, only manually opted experiments are active.'); + } + + if (this._optOutFrom.includes('All')) { + // We prioritize opt out first + traceLog(l10n.t("Experiment '{0}' is inactive", 'All')); + + // Since we are in the Opt Out all case, this means when checking for experiment we + // short circuit and return. So, printing out additional experiment info might cause + // confusion. So skip printing out any specific experiment details to the log. + return; + } + if (this._optInto.includes('All')) { + // Only if 'All' is not in optOut then check if it is in Opt In. + traceLog(l10n.t("Experiment '{0}' is active", 'All')); + + // Similar to the opt out case. If user is opting into to all experiments we short + // circuit the experiment checks. So, skip printing any additional details to the logs. + return; + } + + // Log experiments that users manually opt out, these are experiments which are added using the exp framework. + this._optOutFrom + .filter((exp) => exp !== 'All' && exp.toLowerCase().startsWith('python')) + .forEach((exp) => { + traceLog(l10n.t("Experiment '{0}' is inactive", exp)); + }); + + // Log experiments that users manually opt into, these are experiments which are added using the exp framework. + this._optInto + .filter((exp) => exp !== 'All' && exp.toLowerCase().startsWith('python')) + .forEach((exp) => { + traceLog(l10n.t("Experiment '{0}' is active", exp)); + }); + + if (!experimentsDisabled) { + // Log experiments that users are added to by the exp framework + this.experiments.value.features.forEach((exp) => { + // Filter out experiment groups that are not from the Python extension. + // Filter out experiment groups that are not already opted out or opted into. + if ( + exp.toLowerCase().startsWith('python') && + !this._optOutFrom.includes(exp) && + !this._optInto.includes(exp) + ) { + traceLog(l10n.t("Experiment '{0}' is active", exp)); + } + }); + } + } +} + +/** + * Read accepted experiment settings values from the extension's package.json. + * This function assumes that the `setting` argument is a string array that has a specific set of accepted values. + * + * Accessing the values is done via these keys: + * -> "contributes" -> "configuration" -> "properties" -> -> "items" -> "enum" + * + * @param setting The setting we want to read the values of. + * @param packageJson The content of `package.json`, as a JSON object. + * + * @returns An array containing all accepted values for the setting, or [] if there were none. + */ +function readEnumValues(setting: string, packageJson: Record): string[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const settingProperties = (packageJson.contributes as any).configuration.properties[setting]; + + if (settingProperties) { + return settingProperties.items.enum ?? []; } + + return []; +} + +/** + * Send telemetry on experiments that have been manually opted into or opted-out from. + * The telemetry will only contain values that are present in the list of accepted values for these settings. + * + * @param optedIn The list of experiments opted into. + * @param optedOut The list of experiments opted out from. + * @param packageJson The content of `package.json`, as a JSON object. + */ +function sendOptInOptOutTelemetry(optedIn: string[], optedOut: string[], packageJson: Record): void { + const optedInEnumValues = readEnumValues('python.experiments.optInto', packageJson); + const optedOutEnumValues = readEnumValues('python.experiments.optOutFrom', packageJson); + + const sanitizedOptedIn = optedIn.filter((exp) => optedInEnumValues.includes(exp)); + const sanitizedOptedOut = optedOut.filter((exp) => optedOutEnumValues.includes(exp)); + + JSON.stringify(sanitizedOptedIn.sort()); + + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS, undefined, { + optedInto: JSON.stringify(sanitizedOptedIn.sort()), + optedOutFrom: JSON.stringify(sanitizedOptedOut.sort()), + }); } diff --git a/src/client/common/experiments/telemetry.ts b/src/client/common/experiments/telemetry.ts index 8e0ace9f7a42..bcc9a9c02005 100644 --- a/src/client/common/experiments/telemetry.ts +++ b/src/client/common/experiments/telemetry.ts @@ -10,7 +10,7 @@ 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); } @@ -20,7 +20,6 @@ export class ExperimentationTelemetry implements IExperimentationTelemetry { 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 index a41675749ab9..957ec99a7ce1 100644 --- a/src/client/common/extensions.ts +++ b/src/client/common/extensions.ts @@ -1,31 +1,18 @@ // 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 +// eslint-disable-next-line @typescript-eslint/no-unused-vars 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; + toCommandArgumentForPythonExt(): 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; + fileToCommandArgumentForPythonExt(): string; /** * String.format() implementation. * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. @@ -39,46 +26,30 @@ declare interface 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 { +String.prototype.toCommandArgumentForPythonExt = function (this: string): string { if (!this) { return this; } - return this.indexOf(' ') >= 0 && !this.startsWith('"') && !this.endsWith('"') ? `"${this}"` : this.toString(); + return (this.indexOf(' ') >= 0 || this.indexOf('&') >= 0 || this.indexOf('(') >= 0 || 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 { +String.prototype.fileToCommandArgumentForPythonExt = function (this: string): string { if (!this) { return this; } - return this.toCommandArgument().replace(/\\/g, '/'); + return this.toCommandArgumentForPythonExt().replace(/\\/g, '/'); }; /** @@ -92,20 +63,11 @@ String.prototype.trimQuotes = function (this: string): string { 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(() => {}); + return this.catch(() => {}); }; if (!String.prototype.format) { diff --git a/src/client/common/featureDeprecationManager.ts b/src/client/common/featureDeprecationManager.ts deleted file mode 100644 index 79a4af0cd566..000000000000 --- a/src/client/common/featureDeprecationManager.ts +++ /dev/null @@ -1,153 +0,0 @@ -// 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 index 2fd69900bd32..52eeb1e087aa 100644 --- a/src/client/common/helpers.ts +++ b/src/client/common/helpers.ts @@ -2,13 +2,13 @@ // Licensed under the MIT License. 'use strict'; +import * as os from 'os'; -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; @@ -21,19 +21,6 @@ export function isNotInstalledError(error: Error): boolean { 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; - }; +export function untildify(path: string): string { + return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`); } diff --git a/src/client/common/insidersBuild/downloadChannelRules.ts b/src/client/common/insidersBuild/downloadChannelRules.ts deleted file mode 100644 index 90379a37f99b..000000000000 --- a/src/client/common/insidersBuild/downloadChannelRules.ts +++ /dev/null @@ -1,66 +0,0 @@ -// 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 deleted file mode 100644 index c0096c080045..000000000000 --- a/src/client/common/insidersBuild/downloadChannelService.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index c5fa8200d45b..000000000000 --- a/src/client/common/insidersBuild/insidersExtensionPrompt.ts +++ /dev/null @@ -1,75 +0,0 @@ -// 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 deleted file mode 100644 index f7896cd3e4f7..000000000000 --- a/src/client/common/insidersBuild/insidersExtensionService.ts +++ /dev/null @@ -1,135 +0,0 @@ -// 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 deleted file mode 100644 index 5c47c63ce1ff..000000000000 --- a/src/client/common/insidersBuild/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -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 index de57f4dbbe56..d2950859ab80 100644 --- a/src/client/common/installer/channelManager.ts +++ b/src/client/common/installer/channelManager.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { InterpreterType } from '../../pythonEnvironments/info'; +import { EnvironmentType } from '../../pythonEnvironments/info'; import { IApplicationShell } from '../application/types'; import { IPlatformService } from '../platform/types'; import { Product } from '../types'; @@ -20,7 +20,7 @@ export class InstallationChannelManager implements IInstallationChannelManager { public async getInstallationChannel( product: Product, - resource?: InterpreterUri + resource?: InterpreterUri, ): Promise { const channels = await this.getInstallationChannels(resource); if (channels.length === 1) { @@ -39,13 +39,13 @@ export class InstallationChannelManager implements IInstallationChannelManager { return { label: `Install using ${installer.displayName}`, description: '', - installer + installer, }; }); const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, - placeHolder + placeHolder, }); return selection ? selection.installer : undefined; } @@ -84,18 +84,18 @@ export class InstallationChannelManager implements IInstallationChannelManager { const appShell = this.serviceContainer.get(IApplicationShell); const search = 'Search for help'; let result: string | undefined; - if (interpreter.type === InterpreterType.Conda) { - result = await appShell.showErrorMessage(Installer.noCondaOrPipInstaller(), Installer.searchForHelp()); + if (interpreter.envType === EnvironmentType.Conda) { + result = await appShell.showErrorMessage(Installer.noCondaOrPipInstaller, Installer.searchForHelp); } else { - result = await appShell.showErrorMessage(Installer.noPipInstaller(), Installer.searchForHelp()); + 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.type === InterpreterType.Conda ? 'Conda' : '' - }` + interpreter.envType === EnvironmentType.Conda ? 'Conda' : '' + }`, ); } } diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index 4eb573eed4a3..fbb3dcf183ef 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -1,13 +1,16 @@ +/* eslint-disable class-methods-use-this */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { ICondaService } from '../../interpreter/contracts'; +import { ICondaService, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { ExecutionInfo, IConfigurationService } from '../types'; +import { getEnvPath } from '../../pythonEnvironments/base/info/env'; +import { ModuleInstallerType } from '../../pythonEnvironments/info'; +import { ExecutionInfo, IConfigurationService, Product } from '../types'; import { isResource } from '../utils/misc'; -import { ModuleInstaller } from './moduleInstaller'; -import { InterpreterUri } from './types'; +import { ModuleInstaller, translateProductToModule } from './moduleInstaller'; +import { InterpreterUri, ModuleInstallFlags } from './types'; /** * A Python module installer for a conda environment. @@ -16,6 +19,9 @@ import { InterpreterUri } from './types'; export class CondaInstaller extends ModuleInstaller { public _isCondaAvailable: boolean | undefined; + // Unfortunately inversify requires the number of args in constructor to be explictly + // specified as more than its base class. So we need the constructor. + // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); } @@ -24,12 +30,16 @@ export class CondaInstaller extends ModuleInstaller { return 'Conda'; } - public get displayName() { + public get displayName(): string { return 'Conda'; } + public get type(): ModuleInstallerType { + return ModuleInstallerType.Conda; + } + public get priority(): number { - return 0; + return 10; } /** @@ -56,30 +66,52 @@ export class CondaInstaller extends ModuleInstaller { /** * Return the commandline args needed to install the module. */ - protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { + protected async getExecutionInfo( + moduleName: string, + resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise { const condaService = this.serviceContainer.get(ICondaService); - const condaFile = await condaService.getCondaFile(); + // Installation using `conda.exe` sometimes fails with a HTTP error on Windows: + // https://github.com/conda/conda/issues/11399 + // Execute in a shell which uses a `conda.bat` file instead, using which installation works. + const useShell = true; + const condaFile = await condaService.getCondaFile(useShell); const pythonPath = isResource(resource) ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath - : resource.path; - const info = await condaService.getCondaEnvironment(pythonPath); - const args = ['install']; + : getEnvPath(resource.path, resource.envPath).path ?? ''; + const condaLocatorService = this.serviceContainer.get(IComponentAdapter); + const info = await condaLocatorService.getCondaEnvironment(pythonPath); + const args = [flags & ModuleInstallFlags.upgrade ? 'update' : 'install']; + // Found that using conda-forge is best at packages like tensorboard & ipykernel which seem to get updated first on conda-forge + // https://github.com/microsoft/vscode-jupyter/issues/7787 & https://github.com/microsoft/vscode-python/issues/17628 + // Do this just for the datascience packages. + if ([Product.tensorboard].map(translateProductToModule).includes(moduleName)) { + args.push('-c', 'conda-forge'); + } if (info && info.name) { // If we have the name of the conda environment, then use that. args.push('--name'); - args.push(info.name!.toCommandArgument()); + args.push(info.name.toCommandArgumentForPythonExt()); } else if (info && info.path) { // Else provide the full path to the environment path. args.push('--prefix'); - args.push(info.path.fileToCommandArgument()); + args.push(info.path.fileToCommandArgumentForPythonExt()); + } + if (flags & ModuleInstallFlags.updateDependencies) { + args.push('--update-deps'); + } + if (flags & ModuleInstallFlags.reInstall) { + args.push('--force-reinstall'); } args.push(moduleName); args.push('-y'); return { args, - execPath: condaFile + execPath: condaFile, + useShell, }; } @@ -87,10 +119,10 @@ export class CondaInstaller extends ModuleInstaller { * Is the provided interprter a conda environment */ private async isCurrentEnvironmentACondaEnvironment(resource?: InterpreterUri): Promise { - const condaService = this.serviceContainer.get(ICondaService); + const condaService = this.serviceContainer.get(IComponentAdapter); const pythonPath = isResource(resource) ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath - : resource.path; + : getEnvPath(resource.path, resource.envPath).path ?? ''; return condaService.isCondaEnvironment(pythonPath); } } diff --git a/src/client/common/installer/extensionBuildInstaller.ts b/src/client/common/installer/extensionBuildInstaller.ts deleted file mode 100644 index 1e6aee90ced0..000000000000 --- a/src/client/common/installer/extensionBuildInstaller.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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 index c88aa9774cc7..9dacb623c606 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -3,38 +3,53 @@ import { injectable } from 'inversify'; import * as path from 'path'; -import { CancellationToken, OutputChannel, ProgressLocation, ProgressOptions } from 'vscode'; +import { CancellationToken, l10n, ProgressLocation, ProgressOptions } from 'vscode'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { InterpreterType } from '../../pythonEnvironments/info'; +import { traceError, traceLog } from '../../logging'; +import { EnvironmentType, ModuleInstallerType, virtualEnvTypes } 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 { IProcessServiceFactory } from '../process/types'; +import { ITerminalServiceFactory, TerminalCreationOptions } from '../terminal/types'; +import { ExecutionInfo, IConfigurationService, ILogOutputChannel, Product } from '../types'; import { isResource } from '../utils/misc'; -import { IModuleInstaller, InterpreterUri } from './types'; +import { ProductNames } from './productNames'; +import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types'; @injectable() export abstract class ModuleInstaller implements IModuleInstaller { public abstract get priority(): number; + public abstract get name(): string; + public abstract get displayName(): string; + public abstract get type(): ModuleInstallerType; + constructor(protected serviceContainer: IServiceContainer) {} - public async installModule(name: string, resource?: InterpreterUri, cancel?: CancellationToken): Promise { - sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { installer: this.displayName }); + public async installModule( + productOrModuleName: Product | string, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise { + const shouldExecuteInTerminal = !options?.installAsProcess; + const name = + typeof productOrModuleName === 'string' + ? productOrModuleName + : translateProductToModule(productOrModuleName); + const productName = typeof productOrModuleName === 'string' ? name : ProductNames.get(productOrModuleName); + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { installer: this.displayName, productName }); const uri = isResource(resource) ? resource : undefined; - const executionInfo = await this.getExecutionInfo(name, resource); - const terminalService = this.serviceContainer - .get(ITerminalServiceFactory) - .getTerminalService(uri); + const executionInfo = await this.getExecutionInfo(name, resource, flags); + const install = async (token?: CancellationToken) => { const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource); if (executionInfo.moduleName) { @@ -45,54 +60,102 @@ export abstract class ModuleInstaller implements IModuleInstaller { const interpreter = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource; - const pythonPath = isResource(resource) ? settings.pythonPath : resource.path; + const interpreterPath = interpreter?.path ?? settings.pythonPath; + const pythonPath = isResource(resource) ? interpreterPath : resource.path; const args = internalPython.execModule(executionInfo.moduleName, executionInfoArgs); - if (!interpreter || interpreter.type !== InterpreterType.Unknown) { - await terminalService.sendCommand(pythonPath, args, token); + if (!interpreter || interpreter.envType !== EnvironmentType.Unknown) { + await this.executeCommand( + shouldExecuteInTerminal, + resource, + pythonPath, + args, + token, + executionInfo.useShell, + ); } 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); + await this.executeCommand( + shouldExecuteInTerminal, + resource, + pythonPath, + args, + token, + executionInfo.useShell, + ); } + } else if (name === translateProductToModule(Product.pip)) { + // Pip should always be installed into the specified environment. + await this.executeCommand( + shouldExecuteInTerminal, + resource, + pythonPath, + args, + token, + executionInfo.useShell, + ); + } else if (virtualEnvTypes.includes(interpreter.envType)) { + await this.executeCommand( + shouldExecuteInTerminal, + resource, + pythonPath, + args, + token, + executionInfo.useShell, + ); } else { - await terminalService.sendCommand(pythonPath, args.concat(['--user']), token); + await this.executeCommand( + shouldExecuteInTerminal, + resource, + pythonPath, + args.concat(['--user']), + token, + executionInfo.useShell, + ); } } else { - await terminalService.sendCommand(executionInfo.execPath!, executionInfoArgs, token); + await this.executeCommand( + shouldExecuteInTerminal, + resource, + executionInfo.execPath!, + executionInfoArgs, + token, + executionInfo.useShell, + ); } }; // 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) { + if (cancel && !options?.hideProgress) { const shell = this.serviceContainer.get(IApplicationShell); const options: ProgressOptions = { location: ProgressLocation.Notification, cancellable: true, - title: Products.installingModule().format(name) + title: l10n.t('Installing {0}', name), }; await shell.withProgress(options, async (_, token: CancellationToken) => - install(wrapCancellationTokens(token, cancel)) + 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' + name: 'VS Code Python', }; - const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = this.serviceContainer.get(ILogOutputChannel); const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`; - outputChannel.appendLine(''); - outputChannel.appendLine(`[Elevated] ${command}`); - // tslint:disable-next-line:no-require-imports no-var-requires + traceLog(`[Elevated] ${command}`); + const sudo = require('sudo-prompt'); sudo.exec(command, options, async (error: string, stdout: string, stderr: string) => { @@ -102,17 +165,21 @@ export abstract class ModuleInstaller implements IModuleInstaller { } else { outputChannel.show(); if (stdout) { - outputChannel.appendLine(''); - outputChannel.append(stdout); + traceLog(stdout); } if (stderr) { - outputChannel.appendLine(''); - outputChannel.append(`Warning: ${stderr}`); + traceError(`Warning: ${stderr}`); } } }); } - protected abstract getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise; + + protected abstract getExecutionInfo( + moduleName: string, + resource?: InterpreterUri, + flags?: ModuleInstallFlags, + ): Promise; + private async processInstallArgs(args: string[], resource?: InterpreterUri): Promise { const indexOfPylint = args.findIndex((arg) => arg.toUpperCase() === 'PYLINT'); if (indexOfPylint === -1) { @@ -129,4 +196,66 @@ export abstract class ModuleInstaller implements IModuleInstaller { } return args; } + + private async executeCommand( + executeInTerminal: boolean, + resource: InterpreterUri | undefined, + command: string, + args: string[], + token: CancellationToken | undefined, + useShell: boolean | undefined, + ) { + const options: TerminalCreationOptions = {}; + if (isResource(resource)) { + options.resource = resource; + } else { + options.interpreter = resource; + } + if (executeInTerminal) { + const terminalService = this.serviceContainer + .get(ITerminalServiceFactory) + .getTerminalService(options); + + terminalService.sendCommand(command, args, token); + } else { + const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + const processService = await processServiceFactory.create(options.resource); + if (useShell) { + const argv = [command, ...args]; + // Concat these together to make a set of quoted strings + const quoted = argv.reduce( + (p, c) => + p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`, + '', + ); + await processService.shellExec(quoted); + } else { + await processService.exec(command, args); + } + } + } +} + +export function translateProductToModule(product: Product): string { + switch (product) { + case Product.pytest: + return 'pytest'; + case Product.unittest: + return 'unittest'; + case Product.tensorboard: + return 'tensorboard'; + case Product.torchProfilerInstallName: + return 'torch-tb-profiler'; + case Product.torchProfilerImportName: + return 'torch_tb_profiler'; + case Product.pip: + return 'pip'; + case Product.ensurepip: + return 'ensurepip'; + case Product.python: + return 'python'; + default: { + throw new Error(`Product ${product} cannot be installed as a Python Module.`); + } + } } diff --git a/src/client/common/installer/pipEnvInstaller.ts b/src/client/common/installer/pipEnvInstaller.ts index f57ed06d9cd8..2c7dece6a298 100644 --- a/src/client/common/installer/pipEnvInstaller.ts +++ b/src/client/common/installer/pipEnvInstaller.ts @@ -2,24 +2,28 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { IInterpreterLocatorService, PIPENV_SERVICE } from '../../interpreter/contracts'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { InterpreterType } from '../../pythonEnvironments/info'; +import { isPipenvEnvironmentRelatedToFolder } from '../../pythonEnvironments/common/environmentManagers/pipenv'; +import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info'; +import { IWorkspaceService } from '../application/types'; import { ExecutionInfo } from '../types'; import { isResource } from '../utils/misc'; import { ModuleInstaller } from './moduleInstaller'; -import { InterpreterUri } from './types'; +import { InterpreterUri, ModuleInstallFlags } from './types'; export const pipenvName = 'pipenv'; @injectable() export class PipEnvInstaller extends ModuleInstaller { - private readonly pipenv: IInterpreterLocatorService; - public get name(): string { return 'pipenv'; } + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pipenv; + } + public get displayName() { return pipenvName; } @@ -29,24 +33,38 @@ export class PipEnvInstaller extends ModuleInstaller { 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; + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + const workspaceFolder = resource + ? this.serviceContainer.get(IWorkspaceService).getWorkspaceFolder(resource) + : undefined; + if (!interpreter || !workspaceFolder || interpreter.envType !== EnvironmentType.Pipenv) { + return false; + } + // Install using `pipenv install` only if the active environment is related to the current folder. + return isPipenvEnvironmentRelatedToFolder(interpreter.path, workspaceFolder.uri.fsPath); } else { - return resource.type === InterpreterType.Pipenv; + return resource.envType === EnvironmentType.Pipenv; } } - protected async getExecutionInfo(moduleName: string, _resource?: InterpreterUri): Promise { - const args = ['install', moduleName, '--dev']; - if (moduleName === 'black') { - args.push('--pre'); - } + protected async getExecutionInfo( + moduleName: string, + _resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise { + // In pipenv the only way to update/upgrade or re-install is update (apart from a complete uninstall and re-install). + const update = + flags & ModuleInstallFlags.reInstall || + flags & ModuleInstallFlags.updateDependencies || + flags & ModuleInstallFlags.upgrade; + const args = [update ? 'update' : 'install', moduleName, '--dev']; return { args: args, - execPath: pipenvName + execPath: pipenvName, }; } } diff --git a/src/client/common/installer/pipInstaller.ts b/src/client/common/installer/pipInstaller.ts index 9b0afd3abbc2..cb0274ea5b31 100644 --- a/src/client/common/installer/pipInstaller.ts +++ b/src/client/common/installer/pipInstaller.ts @@ -3,12 +3,39 @@ import { inject, injectable } from 'inversify'; import { IServiceContainer } from '../../ioc/types'; +import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info'; import { IWorkspaceService } from '../application/types'; import { IPythonExecutionFactory } from '../process/types'; -import { ExecutionInfo } from '../types'; +import { ExecutionInfo, IInstaller, Product } from '../types'; import { isResource } from '../utils/misc'; -import { ModuleInstaller } from './moduleInstaller'; -import { InterpreterUri } from './types'; +import { ModuleInstaller, translateProductToModule } from './moduleInstaller'; +import { InterpreterUri, ModuleInstallFlags } from './types'; +import * as path from 'path'; +import { _SCRIPTS_DIR } from '../process/internal/scripts/constants'; +import { ProductNames } from './productNames'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { isParentPath } from '../platform/fs-paths'; + +async function doesEnvironmentContainPython(serviceContainer: IServiceContainer, resource: InterpreterUri) { + const interpreterService = serviceContainer.get(IInterpreterService); + const environment = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource; + if (!environment) { + return undefined; + } + if ( + environment.envPath?.length && + environment.envType === EnvironmentType.Conda && + !isParentPath(environment?.path, environment.envPath) + ) { + // For conda environments not containing a python interpreter, do not use pip installer due to bugs in `conda run`: + // https://github.com/microsoft/vscode-python/issues/18479#issuecomment-1044427511 + // https://github.com/conda/conda/issues/11211 + return false; + } + return true; +} @injectable() export class PipInstaller extends ModuleInstaller { @@ -16,6 +43,10 @@ export class PipInstaller extends ModuleInstaller { return 'Pip'; } + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pip; + } + public get displayName() { return 'Pip'; } @@ -25,20 +56,73 @@ export class PipInstaller extends ModuleInstaller { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); } - public isSupported(resource?: InterpreterUri): Promise { + public async isSupported(resource?: InterpreterUri): Promise { + if ((await doesEnvironmentContainPython(this.serviceContainer, resource)) === false) { + return false; + } return this.isPipAvailable(resource); } - protected async getExecutionInfo(moduleName: string, _resource?: InterpreterUri): Promise { - const proxyArgs: string[] = []; + protected async getExecutionInfo( + moduleName: string, + resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise { + if (moduleName === translateProductToModule(Product.pip)) { + const version = isResource(resource) + ? '' + : `${resource.version?.major || ''}.${resource.version?.minor || ''}.${resource.version?.patch || ''}`; + const envType = isResource(resource) ? undefined : resource.envType; + + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + requiredInstaller: ModuleInstallerType.Pip, + productName: ProductNames.get(Product.pip), + version, + envType, + }); + + // If `ensurepip` is available, if not, then install pip using the script file. + const installer = this.serviceContainer.get(IInstaller); + if (await installer.isInstalled(Product.ensurepip, resource)) { + return { + args: [], + moduleName: 'ensurepip', + }; + } + + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + requiredInstaller: ModuleInstallerType.Pip, + productName: ProductNames.get(Product.ensurepip), + version, + envType, + }); + + // Return script to install pip. + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = isResource(resource) + ? await interpreterService.getActiveInterpreter(resource) + : resource; + return { + execPath: interpreter ? interpreter.path : 'python', + args: [path.join(_SCRIPTS_DIR, 'get-pip.py')], + }; + } + + const args: string[] = []; const workspaceService = this.serviceContainer.get(IWorkspaceService); const proxy = workspaceService.getConfiguration('http').get('proxy', ''); if (proxy.length > 0) { - proxyArgs.push('--proxy'); - proxyArgs.push(proxy); + args.push('--proxy'); + args.push(proxy); + } + args.push(...['install', '-U']); + if (flags & ModuleInstallFlags.reInstall) { + args.push('--force-reinstall'); } return { - args: [...proxyArgs, 'install', '-U', moduleName], - moduleName: 'pip' + args: [...args, moduleName], + moduleName: 'pip', }; } private isPipAvailable(info?: InterpreterUri): Promise { diff --git a/src/client/common/installer/pixiInstaller.ts b/src/client/common/installer/pixiInstaller.ts new file mode 100644 index 000000000000..8a2278830b51 --- /dev/null +++ b/src/client/common/installer/pixiInstaller.ts @@ -0,0 +1,81 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { getEnvPath } from '../../pythonEnvironments/base/info/env'; +import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info'; +import { ExecutionInfo, IConfigurationService } from '../types'; +import { isResource } from '../utils/misc'; +import { ModuleInstaller } from './moduleInstaller'; +import { InterpreterUri } from './types'; +import { getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi'; + +/** + * A Python module installer for a pixi project. + */ +@injectable() +export class PixiInstaller extends ModuleInstaller { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + ) { + super(serviceContainer); + } + + public get name(): string { + return 'Pixi'; + } + + public get displayName(): string { + return 'pixi'; + } + + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pixi; + } + + public get priority(): number { + return 20; + } + + public async isSupported(resource?: InterpreterUri): Promise { + if (isResource(resource)) { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Pixi) { + return false; + } + + const pixiEnv = await getPixiEnvironmentFromInterpreter(interpreter.path); + return pixiEnv !== undefined; + } + return resource.envType === EnvironmentType.Pixi; + } + + /** + * Return the commandline args needed to install the module. + */ + protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { + const pythonPath = isResource(resource) + ? this.configurationService.getSettings(resource).pythonPath + : getEnvPath(resource.path, resource.envPath).path ?? ''; + + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + const execPath = pixiEnv?.pixi.command; + + let args = ['add', moduleName]; + const manifestPath = pixiEnv?.manifestPath; + if (manifestPath !== undefined) { + args = args.concat(['--manifest-path', manifestPath]); + } + + return { + args, + execPath, + }; + } +} diff --git a/src/client/common/installer/poetryInstaller.ts b/src/client/common/installer/poetryInstaller.ts index 68f759e559b8..5017d0813d98 100644 --- a/src/client/common/installer/poetryInstaller.ts +++ b/src/client/common/installer/poetryInstaller.ts @@ -4,29 +4,36 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { isPoetryEnvironmentRelatedToFolder } from '../../pythonEnvironments/common/environmentManagers/poetry'; +import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info'; 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 { + // eslint-disable-next-line class-methods-use-this public get name(): string { return 'poetry'; } - public get displayName() { + // eslint-disable-next-line class-methods-use-this + public get type(): ModuleInstallerType { + return ModuleInstallerType.Poetry; + } + + // eslint-disable-next-line class-methods-use-this + public get displayName(): string { return poetryName; } + + // eslint-disable-next-line class-methods-use-this public get priority(): number { return 10; } @@ -35,44 +42,38 @@ export class PoetryInstaller extends ModuleInstaller { @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) { + if (!isResource(resource)) { 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); + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + if (!interpreter || !workspaceFolder || interpreter.envType !== EnvironmentType.Poetry) { return false; } + // Install using poetry CLI only if the active poetry environment is related to the current folder. + return isPoetryEnvironmentRelatedToFolder( + interpreter.path, + workspaceFolder.uri.fsPath, + this.configurationService.getSettings(resource).poetryPath, + ); } + 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'); - } + const args = ['add', '--group', 'dev', moduleName]; return { args, - execPath + execPath, }; } } diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 774ce9607f00..831eb33efbc6 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -1,67 +1,77 @@ -// 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'; +/* eslint-disable max-classes-per-file */ + +import { inject, injectable } from 'inversify'; +import * as semver from 'semver'; +import { CancellationToken, l10n, Uri } from 'vscode'; +import '../extensions'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { LinterId } from '../../linters/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { EnvironmentType, ModuleInstallerType, 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 { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; -import { ITerminalServiceFactory } from '../terminal/types'; import { IConfigurationService, IInstaller, InstallerResponse, - IOutputChannel, IPersistentStateFactory, - ModuleNamePurpose, + ProductInstallStatus, Product, - ProductType + ProductType, } from '../types'; +import { Common } from '../utils/localize'; import { isResource, noop } from '../utils/misc'; -import { StopWatch } from '../utils/stopWatch'; +import { translateProductToModule } from './moduleInstaller'; import { ProductNames } from './productNames'; import { + IBaseInstaller, IInstallationChannelManager, IModuleInstaller, + InstallOptions, InterpreterUri, IProductPathService, - IProductService + IProductService, + ModuleInstallFlags, } from './types'; +import { traceError, traceInfo } from '../../logging'; +import { isParentPath } from '../platform/fs-paths'; export { Product } from '../types'; -export const CTagsInsllationScript = - os.platform() === 'darwin' ? 'brew install ctags' : 'sudo apt-get install exuberant-ctags'; +// Products which may not be available to install from certain package registries, keyed by product name +// Installer implementations can check this to determine a suitable installation channel for a product +// This is temporary and can be removed when https://github.com/microsoft/vscode-jupyter/issues/5034 is unblocked +const UnsupportedChannelsForProduct = new Map>([ + [Product.torchProfilerInstallName, new Set([EnvironmentType.Conda, EnvironmentType.Pixi])], +]); -export abstract class BaseInstaller { +abstract class BaseInstaller implements IBaseInstaller { private static readonly PromptPromises = new Map>(); + protected readonly appShell: IApplicationShell; + protected readonly configService: IConfigurationService; - private readonly workspaceService: IWorkspaceService; + + protected readonly workspaceService: IWorkspaceService; + private readonly productService: IProductService; - constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { + protected readonly persistentStateFactory: IPersistentStateFactory; + + constructor(protected serviceContainer: IServiceContainer) { this.appShell = serviceContainer.get(IApplicationShell); this.configService = serviceContainer.get(IConfigurationService); this.workspaceService = serviceContainer.get(IWorkspaceService); this.productService = serviceContainer.get(IProductService); + this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); } public promptToInstall( product: Product, resource?: InterpreterUri, - cancel?: CancellationToken + cancel?: CancellationToken, + flags?: ModuleInstallFlags, ): 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 @@ -72,7 +82,7 @@ export abstract class BaseInstaller { if (BaseInstaller.PromptPromises.has(key)) { return BaseInstaller.PromptPromises.get(key)!; } - const promise = this.promptToInstallImplementation(product, resource, cancel); + const promise = this.promptToInstallImplementation(product, resource, cancel, flags); BaseInstaller.PromptPromises.set(key, promise); promise.then(() => BaseInstaller.PromptPromises.delete(key)).ignoreErrors(); promise.catch(() => BaseInstaller.PromptPromises.delete(key)).ignoreErrors(); @@ -83,7 +93,9 @@ export abstract class BaseInstaller { public async install( product: Product, resource?: InterpreterUri, - cancel?: CancellationToken + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, ): Promise { if (product === Product.unittest) { return InstallerResponse.Installed; @@ -92,20 +104,83 @@ export abstract class BaseInstaller { const channels = this.serviceContainer.get(IInstallationChannelManager); const installer = await channels.getInstallationChannel(product, resource); if (!installer) { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + productName: ProductNames.get(product), + }); 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}`)); + .installModule(product, resource, cancel, flags, options) + .catch((ex) => traceError(`Error in installing the product '${ProductNames.get(product)}', ${ex}`)); + + return this.isInstalled(product, resource).then((isInstalled) => { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: installer.displayName, + productName: ProductNames.get(product), + isInstalled, + }); + return isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore; + }); + } - return this.isInstalled(product, resource).then((isInstalled) => - isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore - ); + /** + * + * @param product A product which supports SemVer versioning. + * @param semVerRequirement A SemVer version requirement. + * @param resource A URI or a PythonEnvironment. + */ + public async isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise { + const version = await this.getProductSemVer(product, resource); + if (!version) { + return ProductInstallStatus.NotInstalled; + } + if (semver.satisfies(version, semVerRequirement)) { + return ProductInstallStatus.Installed; + } + return ProductInstallStatus.NeedsUpgrade; + } + + /** + * + * @param product A product which supports SemVer versioning. + * @param resource A URI or a PythonEnvironment. + */ + private async getProductSemVer(product: Product, resource: InterpreterUri): Promise { + const interpreter = isResource(resource) ? undefined : resource; + const uri = isResource(resource) ? resource : undefined; + const executableName = this.getExecutableNameFromSettings(product, uri); + + const isModule = this.isExecutableAModule(product, uri); + + let version; + if (isModule) { + const pythonProcess = await this.serviceContainer + .get(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: uri, interpreter, allowEnvironmentFetchExceptions: true }); + version = await pythonProcess.getModuleVersion(executableName); + } else { + const process = await this.serviceContainer.get(IProcessServiceFactory).create(uri); + const result = await process.exec(executableName, ['--version'], { mergeStdOutErr: true }); + version = result.stdout.trim(); + } + if (!version) { + return null; + } + try { + return semver.coerce(version); + } catch (e) { + traceError(`Unable to parse version ${version} for product ${product}: `, e); + return null; + } } - public async isInstalled(product: Product, resource?: InterpreterUri): Promise { + public async isInstalled(product: Product, resource?: InterpreterUri): Promise { if (product === Product.unittest) { return true; } @@ -120,235 +195,54 @@ export abstract class BaseInstaller { .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); } + 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 + cancel?: CancellationToken, + flags?: ModuleInstallFlags, ): 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 { + + 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 + cancel?: CancellationToken, + _flags?: ModuleInstallFlags, ): Promise { const productName = ProductNames.get(product)!; const options: string[] = []; - let message = `Test framework ${productName} is not installed. Install?`; + let message = l10n.t('Test framework {0} is not installed. Install?', productName); if (this.isExecutableAModule(product, resource)) { - options.push(...['Yes', 'No']); + options.push(...[Common.bannerLabelYes, Common.bannerLabelNo]); } else { const executable = this.getExecutableNameFromSettings(product, resource); - message = `Path to the ${productName} test framework is invalid (${executable})`; + message = l10n.t('Path to the {0} test framework is invalid ({1})', productName, 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; + return item === Common.bannerLabelYes ? this.install(product, resource, cancel) : InstallerResponse.Ignore; } } @@ -357,7 +251,8 @@ export class DataScienceInstaller extends BaseInstaller { public async install( product: Product, interpreterUri?: InterpreterUri, - cancel?: CancellationToken + cancel?: CancellationToken, + flags?: ModuleInstallFlags, ): Promise { // Precondition if (isResource(interpreterUri)) { @@ -365,86 +260,239 @@ export class DataScienceInstaller extends BaseInstaller { } // At this point we know that `interpreterUri` is of type PythonInterpreter - const interpreter = interpreterUri as PythonInterpreter; + const interpreter = interpreterUri as PythonEnvironment; // Get a list of known installation channels, pip, conda, etc. - const channels: IModuleInstaller[] = await this.serviceContainer + let channels: IModuleInstaller[] = await this.serviceContainer .get(IInstallationChannelManager) - .getInstallationChannels(); + .getInstallationChannels(interpreter); // Pick an installerModule based on whether the interpreter is conda or not. Default is pip. - let installerModule; - if (interpreter.type === 'Conda') { - installerModule = channels.find((v) => v.name === 'Conda'); + const moduleName = translateProductToModule(product); + const version = `${interpreter.version?.major || ''}.${interpreter.version?.minor || ''}.${ + interpreter.version?.patch || '' + }`; + + // If this is a non-conda environment & pip isn't installed, we need to install pip. + // The prompt would have been disabled prior to this point, so we can assume that. + if ( + flags && + flags & ModuleInstallFlags.installPipIfRequired && + interpreter.envType !== EnvironmentType.Conda && + !channels.some((channel) => channel.type === ModuleInstallerType.Pip) + ) { + const installers = this.serviceContainer.getAll(IModuleInstaller); + const pipInstaller = installers.find((installer) => installer.type === ModuleInstallerType.Pip); + if (pipInstaller) { + traceInfo(`Installing pip as its not available to install ${moduleName}.`); + await pipInstaller + .installModule(Product.pip, interpreter, cancel) + .catch((ex) => + traceError( + `Error in installing the module '${moduleName} as Pip could not be installed', ${ex}`, + ), + ); + + await this.isInstalled(Product.pip, interpreter) + .then((isInstalled) => { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: pipInstaller.displayName, + requiredInstaller: ModuleInstallerType.Pip, + version, + envType: interpreter.envType, + isInstalled, + productName: ProductNames.get(Product.pip), + }); + }) + .catch(noop); + + // Refresh the list of channels (pip may be avaialble now). + channels = await this.serviceContainer + .get(IInstallationChannelManager) + .getInstallationChannels(interpreter); + } else { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + requiredInstaller: ModuleInstallerType.Pip, + productName: ProductNames.get(Product.pip), + version, + envType: interpreter.envType, + }); + traceError(`Unable to install pip when its required.`); + } + } + + const isAvailableThroughConda = !UnsupportedChannelsForProduct.get(product)?.has(EnvironmentType.Conda); + let requiredInstaller = ModuleInstallerType.Unknown; + if (interpreter.envType === EnvironmentType.Conda && isAvailableThroughConda) { + requiredInstaller = ModuleInstallerType.Conda; + } else if (interpreter.envType === EnvironmentType.Conda && !isAvailableThroughConda) { + // This case is temporary and can be removed when https://github.com/microsoft/vscode-jupyter/issues/5034 is unblocked + traceInfo( + `Interpreter type is conda but package ${moduleName} is not available through conda, using pip instead.`, + ); + requiredInstaller = ModuleInstallerType.Pip; } else { - installerModule = channels.find((v) => v.name === 'Pip'); + switch (interpreter.envType) { + case EnvironmentType.Pipenv: + requiredInstaller = ModuleInstallerType.Pipenv; + break; + case EnvironmentType.Poetry: + requiredInstaller = ModuleInstallerType.Poetry; + break; + default: + requiredInstaller = ModuleInstallerType.Pip; + } } + const installerModule: IModuleInstaller | undefined = channels.find((v) => v.type === requiredInstaller); - const moduleName = translateProductToModule(product, ModuleNamePurpose.install); if (!installerModule) { this.appShell - .showErrorMessage(localize.DataScience.couldNotInstallLibrary().format(moduleName)) + .showErrorMessage( + l10n.t( + '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.', + moduleName, + ), + ) .then(noop, noop); + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + requiredInstaller, + productName: ProductNames.get(product), + version, + envType: interpreter.envType, + }); return InstallerResponse.Ignore; } await installerModule - .installModule(moduleName, interpreter, cancel) + .installModule(product, interpreter, cancel, flags) .catch((ex) => traceError(`Error in installing the module '${moduleName}', ${ex}`)); - return this.isInstalled(product, interpreter).then((isInstalled) => - isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore - ); + return this.isInstalled(product, interpreter).then((isInstalled) => { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: installerModule.displayName || '', + requiredInstaller, + version, + envType: interpreter.envType, + isInstalled, + productName: ProductNames.get(product), + }); + return isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore; + }); } + + /** + * This method will not get invoked for Jupyter extension. + * Implemented as a backup. + */ protected async promptToInstallImplementation( product: Product, resource?: InterpreterUri, - cancel?: CancellationToken + cancel?: CancellationToken, + _flags?: ModuleInstallFlags, ): Promise { const productName = ProductNames.get(product)!; const item = await this.appShell.showErrorMessage( - localize.DataScience.libraryNotInstalled().format(productName), - 'Yes', - 'No' + l10n.t('Data Science library {0} is not installed. Install?', productName), + Common.bannerLabelYes, + Common.bannerLabelNo, ); - 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; - } + if (item === Common.bannerLabelYes) { + return this.install(product, resource, cancel); + } + return InstallerResponse.Ignore; + } +} + +export class PythonInstaller implements IBaseInstaller { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + + public async isInstalled(product: Product, resource?: InterpreterUri): Promise { + if (product !== Product.python) { + throw new Error(`${product} cannot be installed via conda python installer`); + } + const interpreterService = this.serviceContainer.get(IInterpreterService); + const environment = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource; + if (!environment) { + return true; + } + if ( + environment.envPath?.length && + environment.envType === EnvironmentType.Conda && + !isParentPath(environment?.path, environment.envPath) + ) { + return false; } + return true; + } + + public async install( + product: Product, + resource?: InterpreterUri, + _cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise { + if (product !== Product.python) { + throw new Error(`${product} cannot be installed via python installer`); + } + // Active interpreter is a conda environment which does not contain python, hence install it. + const installers = this.serviceContainer.getAll(IModuleInstaller); + const condaInstaller = installers.find((installer) => installer.type === ModuleInstallerType.Conda); + if (!condaInstaller || !(await condaInstaller.isSupported(resource))) { + traceError('Conda installer not available for installing python in the given environment'); + return InstallerResponse.Ignore; + } + const moduleName = translateProductToModule(product); + await condaInstaller + .installModule(Product.python, resource, undefined, undefined, { installAsProcess: true }) + .catch((ex) => traceError(`Error in installing the module '${moduleName}', ${ex}`)); + return this.isInstalled(product, resource).then((isInstalled) => + isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore, + ); + } + + // eslint-disable-next-line class-methods-use-this + public async promptToInstall( + _product: Product, + _resource?: InterpreterUri, + _cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise { + // This package is installed directly without any prompt. return InstallerResponse.Ignore; } + + // eslint-disable-next-line class-methods-use-this + public async isProductVersionCompatible( + _product: Product, + _semVerRequirement: string, + _resource?: InterpreterUri, + ): Promise { + return ProductInstallStatus.Installed; + } } @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 - ) { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.productService = serviceContainer.get(IProductService); this.interpreterService = this.serviceContainer.get(IInterpreterService); } - // tslint:disable-next-line:no-empty - public dispose() {} + public dispose(): void { + /** Do nothing. */ + } + public async promptToInstall( product: Product, resource?: InterpreterUri, - cancel?: CancellationToken + cancel?: CancellationToken, + flags?: ModuleInstallFlags, ): Promise { const currentInterpreter = isResource(resource) ? await this.interpreterService.getActiveInterpreter(resource) @@ -452,91 +500,48 @@ export class ProductInstaller implements IInstaller { if (!currentInterpreter) { return InstallerResponse.Ignore; } - return this.createInstaller(product).promptToInstall(product, resource, cancel); + return this.createInstaller(product).promptToInstall(product, resource, cancel, flags); } + + public async isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise { + return this.createInstaller(product).isProductVersionCompatible(product, semVerRequirement, resource); + } + public async install( product: Product, resource?: InterpreterUri, - cancel?: CancellationToken + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, ): Promise { - return this.createInstaller(product).install(product, resource, cancel); + return this.createInstaller(product).install(product, resource, cancel, flags, options); } - public async isInstalled(product: Product, resource?: InterpreterUri): Promise { + + 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); + + // eslint-disable-next-line class-methods-use-this + public translateProductToModuleName(product: Product): string { + return translateProductToModule(product); } - private createInstaller(product: Product): BaseInstaller { + + private createInstaller(product: Product): IBaseInstaller { 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); + return new TestFrameworkInstaller(this.serviceContainer); case ProductType.DataScience: - return new DataScienceInstaller(this.serviceContainer, this.outputChannel); + return new DataScienceInstaller(this.serviceContainer); + case ProductType.Python: + return new PythonInstaller(this.serviceContainer); 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 index 54d478f81d89..00b19ce77ac3 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -3,25 +3,10 @@ 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'); +ProductNames.set(Product.tensorboard, 'tensorboard'); +ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); +ProductNames.set(Product.torchProfilerImportName, 'torch_tb_profiler'); +ProductNames.set(Product.pip, 'pip'); +ProductNames.set(Product.ensurepip, 'ensurepip'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts index 7ef9bd801d5d..b06e4b7a48a9 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -3,16 +3,12 @@ '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 { ITestingService } from '../../testing/types'; +import { IConfigurationService, IInstaller, Product } from '../types'; import { IProductPathService } from './types'; @injectable() @@ -24,14 +20,10 @@ export abstract class BaseProductPathsService implements IProductPathService { 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; - } + public isExecutableAModule(product: Product, resource?: Uri): boolean { let moduleName: string | undefined; try { - moduleName = this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); - // tslint:disable-next-line:no-empty + moduleName = this.productInstaller.translateProductToModuleName(product); } catch {} // User may have customized the module name or provided the fully qualifieid path. @@ -43,74 +35,29 @@ export abstract class BaseProductPathsService implements IProductPathService { } } -@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 testHelper = this.serviceContainer.get(ITestingService); 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); + return this.productInstaller.translateProductToModuleName(product); } 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); + return this.productInstaller.translateProductToModuleName(product); } } diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index 9687529670a0..bf5597cc5859 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -12,28 +12,14 @@ 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); + this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); + this.ProductTypes.set(Product.torchProfilerInstallName, ProductType.DataScience); + this.ProductTypes.set(Product.torchProfilerImportName, ProductType.DataScience); + this.ProductTypes.set(Product.pip, ProductType.DataScience); + this.ProductTypes.set(Product.ensurepip, ProductType.DataScience); + this.ProductTypes.set(Product.python, ProductType.Python); } 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 index fd1a1fb413bb..1e273ada818c 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -3,77 +3,33 @@ 'use strict'; import { IServiceManager } from '../../ioc/types'; -import { IWebPanelProvider } from '../application/types'; -import { WebPanelProvider } from '../application/webPanels/webPanelProvider'; 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 { PixiInstaller } from './pixiInstaller'; import { PoetryInstaller } from './poetryInstaller'; -import { - CTagsProductPathService, - DataScienceProductPathService, - FormatterProductPathService, - LinterProductPathService, - RefactoringLibraryProductPathService, - TestFrameworkProductPathService -} from './productPath'; +import { DataScienceProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; -import { - IExtensionBuildInstaller, - IInstallationChannelManager, - IModuleInstaller, - INSIDERS_INSTALLER, - IProductPathService, - IProductService, - STABLE_INSTALLER -} from './types'; +import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IModuleInstaller, PixiInstaller); 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 + ProductType.TestFramework, ); serviceManager.addSingleton( IProductPathService, DataScienceProductPathService, - ProductType.DataScience + ProductType.DataScience, ); - serviceManager.addSingleton(IWebPanelProvider, WebPanelProvider); } diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts index 5fa410d96853..a85017ff0092 100644 --- a/src/client/common/installer/types.ts +++ b/src/client/common/installer/types.ts @@ -2,31 +2,56 @@ // Licensed under the MIT License. import { CancellationToken, Uri } from 'vscode'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { Product, ProductType, Resource } from '../types'; +import { ModuleInstallerType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { InstallerResponse, Product, ProductInstallStatus, ProductType, Resource } from '../types'; -export type InterpreterUri = Resource | PythonInterpreter; +export type InterpreterUri = Resource | PythonEnvironment; export const IModuleInstaller = Symbol('IModuleInstaller'); export interface IModuleInstaller { readonly name: string; readonly displayName: string; readonly priority: number; + readonly type: ModuleInstallerType; /** * 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; + installModule( + productOrModuleName: Product | string, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise; isSupported(resource?: InterpreterUri): Promise; } +export const IBaseInstaller = Symbol('IBaseInstaller'); +export interface IBaseInstaller { + install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise; + promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise; + isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise; + isInstalled(product: Product, resource?: InterpreterUri): Promise; +} + export const IPythonInstallation = Symbol('IPythonInstallation'); export interface IPythonInstallation { checkInstallation(): Promise; @@ -45,12 +70,18 @@ export interface IProductService { export const IProductPathService = Symbol('IProductPathService'); export interface IProductPathService { getExecutableNameFromSettings(product: Product, resource?: Uri): string; - isExecutableAModule(product: Product, resource?: Uri): Boolean; + 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; +export enum ModuleInstallFlags { + none = 0, + upgrade = 1, + updateDependencies = 2, + reInstall = 4, + installPipIfRequired = 8, } + +export type InstallOptions = { + installAsProcess?: boolean; + hideProgress?: boolean; +}; diff --git a/src/client/common/interpreterPathService.ts b/src/client/common/interpreterPathService.ts index a5bbc17108b5..935d0bd89ad7 100644 --- a/src/client/common/interpreterPathService.ts +++ b/src/client/common/interpreterPathService.ts @@ -3,13 +3,13 @@ 'use strict'; -import * as fs from 'fs-extra'; +import * as fs from '../common/platform/fs-paths'; import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter, Uri } from 'vscode'; -import { IWorkspaceService } from './application/types'; +import { traceError, traceVerbose } from '../logging'; +import { IApplicationEnvironment, IWorkspaceService } from './application/types'; import { PythonSettings } from './configSettings'; import { isTestExecution } from './constants'; -import { traceError } from './logger'; import { FileSystemPaths } from './platform/fs-paths'; import { IDisposable, @@ -20,12 +20,13 @@ import { IPersistentState, IPersistentStateFactory, IPythonSettings, - Resource + Resource, } from './types'; +import { SystemVariables } from './variables/systemVariables'; -export const workspaceKeysForWhichTheCopyIsDone_Key = 'workspaceKeysForWhichTheCopyIsDone_Key'; -export const workspaceFolderKeysForWhichTheCopyIsDone_Key = 'workspaceFolderKeysForWhichTheCopyIsDone_Key'; -export const isGlobalSettingCopiedKey = 'isGlobalSettingCopiedKey'; +export const remoteWorkspaceKeysForWhichTheCopyIsDone_Key = 'remoteWorkspaceKeysForWhichTheCopyIsDone_Key'; +export const remoteWorkspaceFolderKeysForWhichTheCopyIsDone_Key = 'remoteWorkspaceFolderKeysForWhichTheCopyIsDone_Key'; +export const isRemoteGlobalSettingCopiedKey = 'isRemoteGlobalSettingCopiedKey'; export const defaultInterpreterPathSetting: keyof IPythonSettings = 'defaultInterpreterPath'; const CI_PYTHON_PATH = getCIPythonPath(); @@ -45,7 +46,8 @@ export class InterpreterPathService implements IInterpreterPathService { constructor( @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IDisposableRegistry) disposables: IDisposable[] + @inject(IDisposableRegistry) disposables: IDisposable[], + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, ) { disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); this.fileSystemPaths = FileSystemPaths.withDefaults(); @@ -54,46 +56,58 @@ export class InterpreterPathService implements IInterpreterPathService { public async onDidChangeConfiguration(event: ConfigurationChangeEvent) { if (event.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) { this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global }); + traceVerbose('Interpreter Path updated', `python.${defaultInterpreterPathSetting}`); } } - public inspect(resource: Resource): InspectInterpreterSettingType { + public inspect(resource: Resource, useOldKey = false): 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 + this.getSettingKey(resource, ConfigurationTarget.WorkspaceFolder, useOldKey), + undefined, ); workspaceSetting = this.persistentStateFactory.createGlobalPersistentState( - this.getSettingKey(resource, ConfigurationTarget.Workspace), - undefined + this.getSettingKey(resource, ConfigurationTarget.Workspace, useOldKey), + undefined, ); } - const globalValue = this.workspaceService.getConfiguration('python')!.inspect('defaultInterpreterPath')! - .globalValue; + const defaultInterpreterPath: InspectInterpreterSettingType = + this.workspaceService.getConfiguration('python', resource)?.inspect('defaultInterpreterPath') ?? {}; return { - globalValue, - workspaceFolderValue: workspaceFolderSetting?.value, - workspaceValue: workspaceSetting?.value + globalValue: defaultInterpreterPath.globalValue, + workspaceFolderValue: + !workspaceFolderSetting?.value || workspaceFolderSetting?.value === 'python' + ? defaultInterpreterPath.workspaceFolderValue + : workspaceFolderSetting.value, + workspaceValue: + !workspaceSetting?.value || workspaceSetting?.value === 'python' + ? defaultInterpreterPath.workspaceValue + : workspaceSetting.value, }; } public get(resource: Resource): string { const settings = this.inspect(resource); - return ( + const value = settings.workspaceFolderValue || settings.workspaceValue || settings.globalValue || - (isTestExecution() ? CI_PYTHON_PATH : 'python') + (isTestExecution() ? CI_PYTHON_PATH : 'python'); + const systemVariables = new SystemVariables( + undefined, + this.workspaceService.getWorkspaceFolder(resource)?.uri.fsPath, + this.workspaceService, ); + return systemVariables.resolveAny(value)!; } public async update( resource: Resource, configTarget: ConfigurationTarget, - pythonPath: string | undefined + pythonPath: string | undefined, ): Promise { resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; if (configTarget === ConfigurationTarget.Global) { @@ -101,7 +115,6 @@ export class InterpreterPathService implements IInterpreterPathService { const globalValue = pythonConfig.inspect('defaultInterpreterPath')!.globalValue; if (globalValue !== pythonPath) { await pythonConfig.update('defaultInterpreterPath', pythonPath, true); - this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget }); } return; } @@ -112,17 +125,19 @@ export class InterpreterPathService implements IInterpreterPathService { const settingKey = this.getSettingKey(resource, configTarget); const persistentSetting = this.persistentStateFactory.createGlobalPersistentState( settingKey, - undefined + undefined, ); if (persistentSetting.value !== pythonPath) { await persistentSetting.updateValue(pythonPath); this._didChangeInterpreterEmitter.fire({ uri: resource, configTarget }); + traceVerbose('Interpreter Path updated', settingKey, pythonPath); } } public getSettingKey( resource: Uri, - configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder, + useOldKey = false, ): string { let settingKey: string; const folderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource); @@ -131,21 +146,24 @@ export class InterpreterPathService implements IInterpreterPathService { } else { settingKey = this.workspaceService.workspaceFile ? `WORKSPACE_INTERPRETER_PATH_${this.fileSystemPaths.normCase( - this.workspaceService.workspaceFile.fsPath + this.workspaceService.workspaceFile.fsPath, )}` : // Only a single folder is opened, use fsPath of the folder as key `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; } + if (!useOldKey && this.appEnvironment.remoteName) { + return `${this.appEnvironment.remoteName}_${settingKey}`; + } return settingKey; } public async copyOldInterpreterStorageValuesToNew(resource: Resource): Promise { resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; - const oldSettings = this.workspaceService.getConfiguration('python', resource).inspect('pythonPath')!; + const oldSettings = this.inspect(resource, true); await Promise.all([ this._copyWorkspaceFolderValueToNewStorage(resource, oldSettings.workspaceFolderValue), this._copyWorkspaceValueToNewStorage(resource, oldSettings.workspaceValue), - this._moveGlobalSettingValueToNewStorage(oldSettings.globalValue) + this._moveGlobalSettingValueToNewStorage(oldSettings.globalValue), ]); } @@ -157,8 +175,8 @@ export class InterpreterPathService implements IInterpreterPathService { return; } const flaggedWorkspaceFolderKeysStorage = this.persistentStateFactory.createGlobalPersistentState( - workspaceFolderKeysForWhichTheCopyIsDone_Key, - [] + remoteWorkspaceFolderKeysForWhichTheCopyIsDone_Key, + [], ); const flaggedWorkspaceFolderKeys = flaggedWorkspaceFolderKeysStorage.value; const shouldUpdateWorkspaceFolderSetting = !flaggedWorkspaceFolderKeys.includes(workspaceFolderKey); @@ -177,8 +195,8 @@ export class InterpreterPathService implements IInterpreterPathService { return; } const flaggedWorkspaceKeysStorage = this.persistentStateFactory.createGlobalPersistentState( - workspaceKeysForWhichTheCopyIsDone_Key, - [] + remoteWorkspaceKeysForWhichTheCopyIsDone_Key, + [], ); const flaggedWorkspaceKeys = flaggedWorkspaceKeysStorage.value; const shouldUpdateWorkspaceSetting = !flaggedWorkspaceKeys.includes(workspaceKey); @@ -191,16 +209,12 @@ export class InterpreterPathService implements IInterpreterPathService { 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 + isRemoteGlobalSettingCopiedKey, + 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 deleted file mode 100644 index d332dd6028e5..000000000000 --- a/src/client/common/logger.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 deleted file mode 100644 index 0881b37f3cfb..000000000000 --- a/src/client/common/markdown/restTextConverter.ts +++ /dev/null @@ -1,247 +0,0 @@ -// 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 index 74c629c7d592..115df0f2969c 100644 --- a/src/client/common/net/browser.ts +++ b/src/client/common/net/browser.ts @@ -3,8 +3,6 @@ 'use strict'; -// tslint:disable:no-var-requires - import { injectable } from 'inversify'; import { env, Uri } from 'vscode'; import { IBrowserService } from '../types'; diff --git a/src/client/common/net/fileDownloader.ts b/src/client/common/net/fileDownloader.ts deleted file mode 100644 index 61499d2c28b7..000000000000 --- a/src/client/common/net/fileDownloader.ts +++ /dev/null @@ -1,107 +0,0 @@ -// 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 deleted file mode 100644 index adceb9b1e8a3..000000000000 --- a/src/client/common/net/httpClient.ts +++ /dev/null @@ -1,78 +0,0 @@ -// 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 index 0576d3026fb5..b046cdceaf96 100644 --- a/src/client/common/net/socket/SocketStream.ts +++ b/src/client/common/net/socket/SocketStream.ts @@ -1,13 +1,13 @@ '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 + int64, } export class SocketStream { @@ -26,7 +26,7 @@ export class SocketStream { this.socket.write(buffer); } public WriteString(value: string) { - const stringBuffer = new Buffer(value, 'utf-8'); + const stringBuffer = Buffer.from(value, 'utf-8'); this.WriteInt32(stringBuffer.length); if (stringBuffer.length > 0) { this.socket.write(stringBuffer); @@ -81,7 +81,7 @@ export class SocketStream { this.buffer = additionalData; return; } - const newBuffer = new Buffer(this.buffer.length + additionalData.length); + const newBuffer = Buffer.alloc(this.buffer.length + additionalData.length); this.buffer.copy(newBuffer); additionalData.copy(newBuffer, this.buffer.length); this.buffer = newBuffer; @@ -119,7 +119,7 @@ export class SocketStream { throw new Error('IOException() - Socket.ReadString failed to read string type;'); } - const type = new Buffer([byteRead]).toString(); + const type = Buffer.from([byteRead]).toString(); let isUnicode = false; switch (type) { case 'N': // null string diff --git a/src/client/common/net/socket/socketCallbackHandler.ts b/src/client/common/net/socket/socketCallbackHandler.ts index 55bf49e7b27d..82fe3ec1ae0d 100644 --- a/src/client/common/net/socket/socketCallbackHandler.ts +++ b/src/client/common/net/socket/socketCallbackHandler.ts @@ -1,5 +1,3 @@ -// tslint:disable:quotemark ordered-imports member-ordering one-line prefer-const - 'use strict'; import * as net from 'net'; diff --git a/src/client/common/net/socket/socketServer.ts b/src/client/common/net/socket/socketServer.ts index ed0b37367f42..c0e13a412d7d 100644 --- a/src/client/common/net/socket/socketServer.ts +++ b/src/client/common/net/socket/socketServer.ts @@ -25,7 +25,6 @@ export class SocketServer extends EventEmitter implements ISocketServer { } try { this.socketServer.close(); - // tslint:disable-next-line:no-empty } catch (ex) {} this.socketServer = undefined; } diff --git a/src/client/common/nuget/azureBlobStoreNugetRepository.ts b/src/client/common/nuget/azureBlobStoreNugetRepository.ts deleted file mode 100644 index ae19c65e81be..000000000000 --- a/src/client/common/nuget/azureBlobStoreNugetRepository.ts +++ /dev/null @@ -1,121 +0,0 @@ -// 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 deleted file mode 100644 index 08524d7c3068..000000000000 --- a/src/client/common/nuget/nugetRepository.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index a0a974dfd64d..000000000000 --- a/src/client/common/nuget/nugetService.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index dcde51025138..000000000000 --- a/src/client/common/nuget/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -// 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 deleted file mode 100644 index 648cfa8029a9..000000000000 --- a/src/client/common/open.ts +++ /dev/null @@ -1,85 +0,0 @@ -'use strict'; - -//https://github.com/sindresorhus/opn/blob/master/index.js -//Modified as this uses target as an argument - -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).wait = true; - } - - 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 && Object.keys(opts.env).length > 0) { - cpOpts.env = opts.env; - } - - if (Array.isArray(opts.app)) { - appArgs = opts.app.slice(1); - opts.app = opts.app[0]; - } - - if (process.platform === 'darwin') { - const sudoPrefix = opts.sudo === true ? 'sudo ' : ''; - cmd = 'osascript'; - 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'); - - if (opts.wait) { - args.push('/wait'); - } - - if (opts.app) { - args.push(opts.app); - } - - if (appArgs.length > 0) { - args = args.concat(appArgs); - } - } else { - cmd = 'gnome-terminal'; - const sudoPrefix = opts.sudo === true ? 'sudo ' : ''; - args = ['-x', 'sh', '-c', `"${sudoPrefix}${opts.app}" ${appArgs.join(' ')}`]; - } - - const cp = childProcess.spawn(cmd, args, cpOpts); - - if (opts.wait) { - return new Promise(function (resolve, reject) { - cp.once('error', reject); - - cp.once('close', function (code) { - if (code > 0) { - reject(new Error(`Exited with code ${code}`)); - return; - } - - resolve(cp); - }); - }); - } - - cp.unref(); - - return Promise.resolve(cp); -} diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index ed03b60ddb77..3f9c17657cf4 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -3,16 +3,71 @@ 'use strict'; -import { inject, injectable, named } from 'inversify'; +import { inject, injectable, named, optional } from 'inversify'; import { Memento } from 'vscode'; -import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { traceError } from '../logging'; +import { ICommandManager } from './application/types'; +import { Commands } from './constants'; +import { + GLOBAL_MEMENTO, + IExtensionContext, + IMemento, + IPersistentState, + IPersistentStateFactory, + WORKSPACE_MEMENTO, +} from './types'; +import { cache } from './utils/decorators'; +import { noop } from './utils/misc'; +import { clearCacheDirectory } from '../pythonEnvironments/base/locators/common/nativePythonFinder'; +import { clearCache, useEnvExtension } from '../envExt/api.internal'; + +let _workspaceState: Memento | undefined; +const _workspaceKeys: string[] = []; +export function initializePersistentStateForTriggers(context: IExtensionContext) { + _workspaceState = context.workspaceState; +} + +export function getWorkspaceStateValue(key: string, defaultValue?: T): T | undefined { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + if (defaultValue === undefined) { + return _workspaceState.get(key); + } + return _workspaceState.get(key, defaultValue); +} + +export async function updateWorkspaceStateValue(key: string, value: T): Promise { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + try { + _workspaceKeys.push(key); + await _workspaceState.update(key, value); + const after = getWorkspaceStateValue(key); + if (JSON.stringify(after) !== JSON.stringify(value)) { + await _workspaceState.update(key, undefined); + await _workspaceState.update(key, value); + traceError('Error while updating workspace state for key:', key); + } + } catch (ex) { + traceError(`Error while updating workspace state for key [${key}]:`, ex); + } +} + +async function clearWorkspaceState(): Promise { + if (_workspaceState !== undefined) { + await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined))); + } +} export class PersistentState implements IPersistentState { constructor( - private storage: Memento, + public readonly storage: Memento, private key: string, private defaultValue?: T, - private expiryDurationMs?: number + private expiryDurationMs?: number, ) {} public get value(): T { @@ -28,33 +83,156 @@ export class PersistentState implements IPersistentState { } } - 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); + public async updateValue(newValue: T, retryOnce = true): Promise { + try { + if (this.expiryDurationMs) { + await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs }); + } else { + await this.storage.update(this.key, newValue); + } + if (retryOnce && JSON.stringify(this.value) != JSON.stringify(newValue)) { + // Due to a VSCode bug sometimes the changes are not reflected in the storage, atleast not immediately. + // It is noticed however that if we reset the storage first and then update it, it works. + // https://github.com/microsoft/vscode/issues/171827 + await this.updateValue(undefined as any, false); + await this.updateValue(newValue, false); + } + } catch (ex) { + traceError('Error while updating storage for key:', this.key, ex); } } } +export const GLOBAL_PERSISTENT_KEYS_DEPRECATED = 'PYTHON_EXTENSION_GLOBAL_STORAGE_KEYS'; +export const WORKSPACE_PERSISTENT_KEYS_DEPRECATED = 'PYTHON_EXTENSION_WORKSPACE_STORAGE_KEYS'; + +export const GLOBAL_PERSISTENT_KEYS = 'PYTHON_GLOBAL_STORAGE_KEYS'; +const WORKSPACE_PERSISTENT_KEYS = 'PYTHON_WORKSPACE_STORAGE_KEYS'; +type KeysStorageType = 'global' | 'workspace'; +export type KeysStorage = { key: string; defaultValue: unknown }; + @injectable() -export class PersistentStateFactory implements IPersistentStateFactory { +export class PersistentStateFactory implements IPersistentStateFactory, IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + public readonly _globalKeysStorage = new PersistentState( + this.globalState, + GLOBAL_PERSISTENT_KEYS, + [], + ); + public readonly _workspaceKeysStorage = new PersistentState( + this.workspaceState, + WORKSPACE_PERSISTENT_KEYS, + [], + ); constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, - @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento + @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento, + @inject(ICommandManager) private cmdManager?: ICommandManager, + @inject(IExtensionContext) @optional() private context?: IExtensionContext, ) {} + + public async activate(): Promise { + this.cmdManager?.registerCommand(Commands.ClearStorage, async () => { + await clearWorkspaceState(); + await this.cleanAllPersistentStates(); + if (useEnvExtension()) { + await clearCache(); + } + }); + const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []); + const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState( + WORKSPACE_PERSISTENT_KEYS_DEPRECATED, + [], + ); + // Old storages have grown to be unusually large due to https://github.com/microsoft/vscode-python/issues/17488, + // so reset them. This line can be removed after a while. + if (globalKeysStorageDeprecated.value.length > 0) { + globalKeysStorageDeprecated.updateValue([]).ignoreErrors(); + } + if (workspaceKeysStorageDeprecated.value.length > 0) { + workspaceKeysStorageDeprecated.updateValue([]).ignoreErrors(); + } + } + public createGlobalPersistentState( key: string, defaultValue?: T, - expiryDurationMs?: number + expiryDurationMs?: number, ): IPersistentState { + this.addKeyToStorage('global', key, defaultValue).ignoreErrors(); return new PersistentState(this.globalState, key, defaultValue, expiryDurationMs); } + public createWorkspacePersistentState( key: string, defaultValue?: T, - expiryDurationMs?: number + expiryDurationMs?: number, ): IPersistentState { + this.addKeyToStorage('workspace', key, defaultValue).ignoreErrors(); return new PersistentState(this.workspaceState, key, defaultValue, expiryDurationMs); } + + /** + * Note we use a decorator to cache the promise returned by this method, so it's only called once. + * It is only cached for the particular arguments passed, so the argument type is simplified here. + */ + @cache(-1, true) + private async addKeyToStorage(keyStorageType: KeysStorageType, key: string, defaultValue?: T) { + const storage = keyStorageType === 'global' ? this._globalKeysStorage : this._workspaceKeysStorage; + const found = storage.value.find((value) => value.key === key); + if (!found) { + await storage.updateValue([{ key, defaultValue }, ...storage.value]); + } + } + + private async cleanAllPersistentStates(): Promise { + const clearCacheDirPromise = this.context ? clearCacheDirectory(this.context).catch() : Promise.resolve(); + await Promise.all( + this._globalKeysStorage.value.map(async (keyContent) => { + const storage = this.createGlobalPersistentState(keyContent.key); + await storage.updateValue(keyContent.defaultValue); + }), + ); + await Promise.all( + this._workspaceKeysStorage.value.map(async (keyContent) => { + const storage = this.createWorkspacePersistentState(keyContent.key); + await storage.updateValue(keyContent.defaultValue); + }), + ); + await this._globalKeysStorage.updateValue([]); + await this._workspaceKeysStorage.updateValue([]); + await clearCacheDirPromise; + this.cmdManager?.executeCommand('workbench.action.reloadWindow').then(noop); + } +} + +///////////////////////////// +// a simpler, alternate API +// for components to use + +export interface IPersistentStorage { + get(): T; + set(value: T): Promise; +} + +/** + * Build a global storage object for the given key. + */ +export function getGlobalStorage(context: IExtensionContext, key: string, defaultValue?: T): IPersistentStorage { + const globalKeysStorage = new PersistentState(context.globalState, GLOBAL_PERSISTENT_KEYS, []); + const found = globalKeysStorage.value.find((value) => value.key === key); + if (!found) { + const newValue = [{ key, defaultValue }, ...globalKeysStorage.value]; + globalKeysStorage.updateValue(newValue).ignoreErrors(); + } + const raw = new PersistentState(context.globalState, key, defaultValue); + return { + // We adapt between PersistentState and IPersistentStorage. + get() { + return raw.value; + }, + set(value: T) { + return raw.updateValue(value); + }, + }; } diff --git a/src/client/common/pipes/namedPipes.ts b/src/client/common/pipes/namedPipes.ts new file mode 100644 index 000000000000..9bffe78f2b9f --- /dev/null +++ b/src/client/common/pipes/namedPipes.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as cp from 'child_process'; +import * as crypto from 'crypto'; +import * as fs from 'fs-extra'; +import * as net from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as rpc from 'vscode-jsonrpc/node'; +import { CancellationError, CancellationToken, Disposable } from 'vscode'; +import { traceVerbose } from '../../logging'; +import { isWindows } from '../utils/platform'; +import { createDeferred } from '../utils/async'; +import { noop } from '../utils/misc'; + +const { XDG_RUNTIME_DIR } = process.env; +export function generateRandomPipeName(prefix: string): string { + // length of 10 picked because of the name length restriction for sockets + const randomSuffix = crypto.randomBytes(10).toString('hex'); + if (prefix.length === 0) { + prefix = 'python-ext-rpc'; + } + + if (process.platform === 'win32') { + return `\\\\.\\pipe\\${prefix}-${randomSuffix}`; + } + + let result; + if (XDG_RUNTIME_DIR) { + result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}`); + } else { + result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}`); + } + + return result; +} + +async function mkfifo(fifoPath: string): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn('mkfifo', [fifoPath]); + proc.on('error', (err) => { + reject(err); + }); + proc.on('exit', (code) => { + if (code === 0) { + resolve(); + } + }); + }); +} + +export async function createWriterPipe(pipeName: string, token?: CancellationToken): Promise { + // windows implementation of FIFO using named pipes + if (isWindows()) { + const deferred = createDeferred(); + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + server.close(); + deferred.resolve(new rpc.SocketMessageWriter(socket, 'utf-8')); + }); + + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + return deferred.promise; + } + // linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const writer = fs.createWriteStream(pipeName, { + encoding: 'utf-8', + }); + return new rpc.StreamMessageWriter(writer, 'utf-8'); +} + +class CombinedReader implements rpc.MessageReader { + private _onError = new rpc.Emitter(); + + private _onClose = new rpc.Emitter(); + + private _onPartialMessage = new rpc.Emitter(); + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + private _callback: rpc.DataCallback = () => {}; + + private _disposables: rpc.Disposable[] = []; + + private _readers: rpc.MessageReader[] = []; + + constructor() { + this._disposables.push(this._onClose, this._onError, this._onPartialMessage); + } + + onError: rpc.Event = this._onError.event; + + onClose: rpc.Event = this._onClose.event; + + onPartialMessage: rpc.Event = this._onPartialMessage.event; + + listen(callback: rpc.DataCallback): rpc.Disposable { + this._callback = callback; + // eslint-disable-next-line no-return-assign, @typescript-eslint/no-empty-function + return new Disposable(() => (this._callback = () => {})); + } + + add(reader: rpc.MessageReader): void { + this._readers.push(reader); + reader.listen((msg) => { + this._callback(msg as rpc.NotificationMessage); + }); + this._disposables.push(reader); + reader.onClose(() => { + this.remove(reader); + if (this._readers.length === 0) { + this._onClose.fire(); + } + }); + reader.onError((e) => { + this.remove(reader); + this._onError.fire(e); + }); + } + + remove(reader: rpc.MessageReader): void { + const found = this._readers.find((r) => r === reader); + if (found) { + this._readers = this._readers.filter((r) => r !== reader); + reader.dispose(); + } + } + + dispose(): void { + this._readers.forEach((r) => r.dispose()); + this._readers = []; + this._disposables.forEach((disposable) => disposable.dispose()); + this._disposables = []; + } +} + +export async function createReaderPipe(pipeName: string, token?: CancellationToken): Promise { + if (isWindows()) { + // windows implementation of FIFO using named pipes + const deferred = createDeferred(); + const combined = new CombinedReader(); + + let refs = 0; + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + refs += 1; + + socket.on('close', () => { + refs -= 1; + if (refs <= 0) { + server.close(); + } + }); + combined.add(new rpc.SocketMessageReader(socket, 'utf-8')); + }); + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + deferred.resolve(combined); + return deferred.promise; + } + // mac/linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const fd = await fs.open(pipeName, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK); + const socket = new net.Socket({ fd }); + const reader = new rpc.SocketMessageReader(socket, 'utf-8'); + socket.on('close', () => { + fs.close(fd).catch(noop); + reader.dispose(); + }); + + return reader; +} diff --git a/src/client/common/platform/constants.ts b/src/client/common/platform/constants.ts deleted file mode 100644 index 2c4a65f80251..000000000000 --- a/src/client/common/platform/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 index 8d3fe1b6bbe3..9533f1bca50c 100644 --- a/src/client/common/platform/errors.ts +++ b/src/client/common/platform/errors.ts @@ -42,7 +42,7 @@ namespace vscErrors { FILE_EXISTS, IS_DIR, NOT_DIR, - NO_PERM + NO_PERM, ]; function errorMatches(err: Error, expectedName: string): boolean | undefined { if (!known.includes(err.name)) { @@ -97,21 +97,23 @@ function isSystemError(err: Error, expectedCode: string): boolean | undefined { } // Return true if the given error is ENOENT. -export function isFileNotFoundError(err: Error): boolean | undefined { - const matched = vscErrors.isFileNotFound(err); +export function isFileNotFoundError(err: unknown | Error): boolean | undefined { + const error = err as Error; + const matched = vscErrors.isFileNotFound(error); if (matched !== undefined) { return matched; } - return isSystemError(err, 'ENOENT'); + return isSystemError(error, 'ENOENT'); } // Return true if the given error is EEXIST. -export function isFileExistsError(err: Error): boolean | undefined { - const matched = vscErrors.isFileExists(err); +export function isFileExistsError(err: unknown | Error): boolean | undefined { + const error = err as Error; + const matched = vscErrors.isFileExists(error); if (matched !== undefined) { return matched; } - return isSystemError(err, 'EEXIST'); + return isSystemError(error, 'EEXIST'); } // Return true if the given error is EISDIR. @@ -133,15 +135,11 @@ export function isNotDirError(err: Error): boolean | undefined { } // Return true if the given error is EACCES. -export function isNoPermissionsError(err: Error): boolean | undefined { - const matched = vscErrors.isNoPermissions(err); +export function isNoPermissionsError(err: unknown | Error): boolean | undefined { + const error = err as Error; + const matched = vscErrors.isNoPermissions(error); 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'); + return isSystemError(error, 'EACCES'); } diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index c79075788416..3e7f441654ec 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -1,8 +1,8 @@ +/* eslint-disable max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; -// tslint:disable:no-suspicious-comment +'use strict'; import { createHash } from 'crypto'; import * as fs from 'fs-extra'; @@ -10,8 +10,9 @@ 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 { traceError } from '../../logging'; +import '../extensions'; +import { convertFileType } from '../utils/filesystem'; import { createDirNotEmptyError, isFileExistsError, isFileNotFoundError, isNoPermissionsError } from './errors'; import { FileSystemPaths, FileSystemPathUtils } from './fs-paths'; import { TemporaryFileSystem } from './fs-temp'; @@ -26,27 +27,10 @@ import { ITempFileSystem, ReadStream, TemporaryFile, - WriteStream + 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; - } -} +const ENCODING = 'utf8'; export function convertStat(old: fs.Stats, filetype: FileType): FileStat { return { @@ -57,27 +41,25 @@ export function convertStat(old: fs.Stats, filetype: FileType): FileStat { // 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) + mtime: Math.round(old.mtimeMs), }; } function filterByFileType( files: [string, FileType][], // the files to filter - fileType: FileType // the file type to look for + 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); + return files.filter( + ([_file, ft]) => ft === FileType.Unknown || ft === (FileType.SymbolicLink & FileType.Unknown), + ); } + return files.filter(([_file, ft]) => (ft & fileType) > 0); } -//========================================== // "raw" filesystem // This is the parts of the vscode.workspace.fs API that we use here. @@ -98,7 +80,7 @@ interface IVSCodeFileSystemAPI { interface IRawFSExtra { lstat(filename: string): Promise; chmod(filePath: string, mode: string | number): Promise; - appendFile(filename: string, data: {}): Promise; + appendFile(filename: string, data: unknown): Promise; // non-async lstatSync(filename: string): fs.Stats; @@ -106,6 +88,7 @@ interface IRawFSExtra { readFileSync(path: string, encoding: string): string; createReadStream(filename: string): ReadStream; createWriteStream(filename: string): WriteStream; + pathExists(filename: string): Promise; } interface IRawPath { @@ -124,14 +107,14 @@ export class RawFileSystem implements IRawFileSystem { // the VS Code FS API to use protected readonly vscfs: IVSCodeFileSystemAPI, // the node FS API to use - protected readonly fsExtra: IRawFSExtra + 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 + fsExtra?: IRawFSExtra, // default: the "fs-extra" module ): RawFileSystem { return new RawFileSystem( paths || FileSystemPaths.withDefaults(), @@ -139,10 +122,14 @@ export class RawFileSystem implements IRawFileSystem { // 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 + fsExtra || (fs as IRawFSExtra), ); } + public async pathExists(filename: string): Promise { + return this.fsExtra.pathExists(filename); + } + public async stat(filename: string): Promise { // Note that, prior to the November release of VS Code, // stat.ctime was always 0. @@ -229,7 +216,7 @@ export class RawFileSystem implements IRawFileSystem { // 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 + overwrite: true, }); } @@ -237,7 +224,7 @@ export class RawFileSystem implements IRawFileSystem { const uri = vscode.Uri.file(filename); return this.vscfs.delete(uri, { recursive: false, - useTrash: false + useTrash: false, }); } @@ -251,7 +238,7 @@ export class RawFileSystem implements IRawFileSystem { } return this.vscfs.delete(uri, { recursive: true, - useTrash: false + useTrash: false, }); } @@ -264,7 +251,7 @@ export class RawFileSystem implements IRawFileSystem { await this.vscfs.stat(uri); return this.vscfs.delete(uri, { recursive: true, - useTrash: false + useTrash: false, }); } @@ -282,7 +269,6 @@ export class RawFileSystem implements IRawFileSystem { }); } - //**************************** // non-async // VS Code has decided to never support any sync functions (aside @@ -319,7 +305,6 @@ export class RawFileSystem implements IRawFileSystem { } } -//========================================== // filesystem "utils" // High-level filesystem operations used by the extension. @@ -330,15 +315,16 @@ export class FileSystemUtils implements IFileSystemUtils { 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 + 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 + globFiles?: (pat: string, options?: { cwd: string }) => Promise, ): FileSystemUtils { pathUtils = pathUtils || FileSystemPathUtils.withDefaults(); return new FileSystemUtils( @@ -347,11 +333,10 @@ export class FileSystemUtils implements IFileSystemUtils { pathUtils.paths, tmp || TemporaryFileSystem.withDefaults(), getHash || getHashString, - globFiles || promisify(glob) + globFiles || promisify(glob.default), ); } - //**************************** // aliases public async createDirectory(directoryPath: string): Promise { @@ -366,7 +351,6 @@ export class FileSystemUtils implements IFileSystemUtils { return this.raw.rmfile(filename); } - //**************************** // helpers public async pathExists( @@ -374,8 +358,12 @@ export class FileSystemUtils implements IFileSystemUtils { 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 + fileType?: FileType, ): Promise { + if (fileType === undefined) { + // Do not need to run stat if not asking for file type. + return this.raw.pathExists(filename); + } let stat: FileStat; try { // Note that we are using stat() rather than lstat(). This @@ -389,18 +377,17 @@ export class FileSystemUtils implements IFileSystemUtils { 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); } @@ -416,11 +403,13 @@ export class FileSystemUtils implements IFileSystemUtils { 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); @@ -454,7 +443,7 @@ export class FileSystemUtils implements IFileSystemUtils { } public async search(globPattern: string, cwd?: string, dot?: boolean): Promise { - // tslint:disable-next-line: no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any let options: any; if (cwd) { options = { ...options, cwd }; @@ -467,7 +456,6 @@ export class FileSystemUtils implements IFileSystemUtils { return Array.isArray(found) ? found : []; } - //**************************** // helpers (non-async) public fileExistsSync(filePath: string): boolean { @@ -483,15 +471,12 @@ export class FileSystemUtils implements IFileSystemUtils { } } -// 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) @@ -500,6 +485,7 @@ 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(); } @@ -507,81 +493,112 @@ export class FileSystem implements IFileSystem { 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 { + + // eslint-disable-next-line @typescript-eslint/ban-types + public async writeFile(filename: string, data: string | Buffer): 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) { + + public async move(src: string, tgt: string): Promise { 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 pathExists(filename: string): Promise { + return this.utils.pathExists(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/fileSystemWatcher.ts b/src/client/common/platform/fileSystemWatcher.ts new file mode 100644 index 000000000000..ef35988d147b --- /dev/null +++ b/src/client/common/platform/fileSystemWatcher.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { RelativePattern, workspace } from 'vscode'; +import { traceVerbose } from '../../logging'; +import { IDisposable } from '../types'; +import { Disposables } from '../utils/resourceLifecycle'; + +/** + * Enumeration of file change types. + */ +export enum FileChangeType { + Changed = 'changed', + Created = 'created', + Deleted = 'deleted', +} + +export function watchLocationForPattern( + baseDir: string, + pattern: string, + callback: (type: FileChangeType, absPath: string) => void, +): IDisposable { + const globPattern = new RelativePattern(baseDir, pattern); + const disposables = new Disposables(); + traceVerbose(`Start watching: ${baseDir} with pattern ${pattern} using VSCode API`); + const watcher = workspace.createFileSystemWatcher(globPattern); + disposables.push(watcher.onDidCreate((e) => callback(FileChangeType.Created, e.fsPath))); + disposables.push(watcher.onDidChange((e) => callback(FileChangeType.Changed, e.fsPath))); + disposables.push(watcher.onDidDelete((e) => callback(FileChangeType.Deleted, e.fsPath))); + return disposables; +} diff --git a/src/client/common/platform/fs-paths.ts b/src/client/common/platform/fs-paths.ts index c2d953dcca87..fa809d31b0b9 100644 --- a/src/client/common/platform/fs-paths.ts +++ b/src/client/common/platform/fs-paths.ts @@ -2,10 +2,11 @@ // Licensed under the MIT License. import * as nodepath from 'path'; +import { getSearchPathEnvVarNames } from '../utils/exec'; +import * as fs from 'fs-extra'; +import * as os from 'os'; 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 { @@ -21,14 +22,14 @@ export class FileSystemPaths implements IFileSystemPaths { // "true" if targeting a case-insensitive host (like Windows) private readonly isCaseInsensitive: boolean, // (effectively) the node "path" module to use - private readonly raw: INodePath + 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 + isCaseInsensitive?: boolean, ): FileSystemPaths { if (isCaseInsensitive === undefined) { isCaseInsensitive = getOSType() === OSType.Windows; @@ -36,7 +37,7 @@ export class FileSystemPaths implements IFileSystemPaths { return new FileSystemPaths( isCaseInsensitive, // Use the actual node "path" module. - nodepath + nodepath, ); } @@ -71,7 +72,7 @@ export class Executables { // the $PATH delimiter to use public readonly delimiter: string, // the OS type to target - private readonly osType: OSType + private readonly osType: OSType, ) {} // Create a new object using common-case default values. // We do not use an alternate constructor because defaults in the @@ -81,12 +82,12 @@ export class Executables { // Use node's value. nodepath.delimiter, // Use the current OS. - getOSType() + getOSType(), ); } public get envVar(): string { - return this.osType === OSType.Windows ? 'Path' : 'PATH'; + return getSearchPathEnvVarNames(this.osType)[0]; } } @@ -104,25 +105,25 @@ export class FileSystemPathUtils implements IFileSystemPathUtils { // 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 + 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 + paths?: IFileSystemPaths, ): FileSystemPathUtils { if (paths === undefined) { paths = FileSystemPaths.withDefaults(); } return new FileSystemPathUtils( // Use the current user's home directory. - untildify('~'), + os.homedir(), paths, Executables.withDefaults(), // Use the actual node "path" module. - nodepath + nodepath, ); } @@ -133,12 +134,237 @@ export class FileSystemPathUtils implements IFileSystemPathUtils { } public getDisplayName(filename: string, cwd?: string): string { - if (cwd && filename.startsWith(cwd)) { + if (cwd && isParentPath(filename, cwd)) { return `.${this.paths.sep}${this.raw.relative(cwd, filename)}`; - } else if (filename.startsWith(this.home)) { + } else if (isParentPath(filename, this.home)) { return `~${this.paths.sep}${this.raw.relative(this.home, filename)}`; } else { return filename; } } } + +export function normCasePath(filePath: string): string { + return normCase(nodepath.normalize(filePath)); +} + +export function normCase(s: string): string { + return getOSType() === OSType.Windows ? s.toUpperCase() : s; +} + +/** + * Returns true if given file path exists within the given parent directory, false otherwise. + * @param filePath File path to check for + * @param parentPath The potential parent path to check for + */ +export function isParentPath(filePath: string, parentPath: string): boolean { + if (!parentPath.endsWith(nodepath.sep)) { + parentPath += nodepath.sep; + } + if (!filePath.endsWith(nodepath.sep)) { + filePath += nodepath.sep; + } + return normCasePath(filePath).startsWith(normCasePath(parentPath)); +} + +export function arePathsSame(path1: string, path2: string): boolean { + return normCasePath(path1) === normCasePath(path2); +} + +export async function copyFile(src: string, dest: string): Promise { + const destDir = nodepath.dirname(dest); + if (!(await fs.pathExists(destDir))) { + await fs.mkdirp(destDir); + } + + await fs.copy(src, dest, { + overwrite: true, + }); +} + +// These function exist so we can stub them out in tests. We can't stub out the fs module directly +// because of the way that sinon does stubbing, so we have these intermediaries instead. +export { Stats, WriteStream, ReadStream, PathLike, Dirent, PathOrFileDescriptor } from 'fs-extra'; + +export function existsSync(path: string): boolean { + return fs.existsSync(path); +} + +export function readFileSync(filePath: string, encoding: BufferEncoding): string; +export function readFileSync(filePath: string): Buffer; +export function readFileSync(filePath: string, options: { encoding: BufferEncoding }): string; +export function readFileSync( + filePath: string, + options?: { encoding: BufferEncoding } | BufferEncoding | undefined, +): string | Buffer { + if (typeof options === 'string') { + return fs.readFileSync(filePath, { encoding: options }); + } + return fs.readFileSync(filePath, options); +} + +export function readJSONSync(filePath: string): any { + return fs.readJSONSync(filePath); +} + +export function readdirSync(path: string): string[]; +export function readdirSync( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): fs.Dirent[]; +export function readdirSync( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: false; + }, +): string[]; +export function readdirSync( + path: fs.PathLike, + options?: fs.ObjectEncodingOptions & { + withFileTypes: boolean; + recursive?: boolean | undefined; + }, +): string[] | fs.Dirent[] { + if (options === undefined || options.withFileTypes === false) { + return fs.readdirSync(path); + } + return fs.readdirSync(path, { ...options, withFileTypes: true }); +} + +export function readlink(path: string): Promise { + return fs.readlink(path); +} + +export function unlink(path: string): Promise { + return fs.unlink(path); +} + +export function symlink(target: string, path: string, type?: fs.SymlinkType): Promise { + return fs.symlink(target, path, type); +} + +export function symlinkSync(target: string, path: string, type?: fs.SymlinkType): void { + return fs.symlinkSync(target, path, type); +} + +export function unlinkSync(path: string): void { + return fs.unlinkSync(path); +} + +export function statSync(path: string): fs.Stats { + return fs.statSync(path); +} + +export function stat(path: string): Promise { + return fs.stat(path); +} + +export function lstat(path: string): Promise { + return fs.lstat(path); +} + +export function chmod(path: string, mod: fs.Mode): Promise { + return fs.chmod(path, mod); +} + +export function createReadStream(path: string): fs.ReadStream { + return fs.createReadStream(path); +} + +export function createWriteStream(path: string): fs.WriteStream { + return fs.createWriteStream(path); +} + +export function pathExistsSync(path: string): boolean { + return fs.pathExistsSync(path); +} + +export function pathExists(absPath: string): Promise { + return fs.pathExists(absPath); +} + +export function createFile(filename: string): Promise { + return fs.createFile(filename); +} + +export function rmdir(path: string, options?: fs.RmDirOptions): Promise { + return fs.rmdir(path, options); +} + +export function remove(path: string): Promise { + return fs.remove(path); +} + +export function readFile(filePath: string, encoding: BufferEncoding): Promise; +export function readFile(filePath: string): Promise; +export function readFile(filePath: string, options: { encoding: BufferEncoding }): Promise; +export function readFile( + filePath: string, + options?: { encoding: BufferEncoding } | BufferEncoding | undefined, +): Promise { + if (typeof options === 'string') { + return fs.readFile(filePath, { encoding: options }); + } + return fs.readFile(filePath, options); +} + +export function readJson(filePath: string): Promise { + return fs.readJson(filePath); +} + +export function writeFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise { + return fs.writeFile(filePath, data, options); +} + +export function mkdir(dirPath: string): Promise { + return fs.mkdir(dirPath); +} + +export function mkdirp(dirPath: string): Promise { + return fs.mkdirp(dirPath); +} + +export function rename(oldPath: string, newPath: string): Promise { + return fs.rename(oldPath, newPath); +} + +export function ensureDir(dirPath: string): Promise { + return fs.ensureDir(dirPath); +} + +export function ensureFile(filePath: string): Promise { + return fs.ensureFile(filePath); +} + +export function ensureSymlink(target: string, filePath: string, type?: fs.SymlinkType): Promise { + return fs.ensureSymlink(target, filePath, type); +} + +export function appendFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise { + return fs.appendFile(filePath, data, options); +} + +export function readdir(path: string): Promise; +export function readdir( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise; +export function readdir( + path: fs.PathLike, + options?: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise { + if (options === undefined) { + return fs.readdir(path); + } + return fs.readdir(path, options); +} + +export function emptyDir(dirPath: string): Promise { + return fs.emptyDir(dirPath); +} diff --git a/src/client/common/platform/fs-temp.ts b/src/client/common/platform/fs-temp.ts index 64d2870a47e4..60dde040f454 100644 --- a/src/client/common/platform/fs-temp.ts +++ b/src/client/common/platform/fs-temp.ts @@ -5,27 +5,19 @@ 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; + fileSync(config?: tmp.Options): tmp.SynchrounousResult; } // 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 + private readonly raw: IRawTempFS, ) {} public static withDefaults(): TemporaryFileSystem { return new TemporaryFileSystem( // Use the actual "tmp" module. - tmp + tmp, ); } @@ -33,17 +25,16 @@ export class TemporaryFileSystem implements ITempFileSystem { public createFile(suffix: string, mode?: number): Promise { const opts = { postfix: suffix, - mode + mode, }; return new Promise((resolve, reject) => { - this.raw.file(opts, (err, filename, _fd, cleanUp) => { - if (err) { - return reject(err); - } - resolve({ - filePath: filename, - dispose: cleanUp - }); + const { name, removeCallback } = this.raw.fileSync(opts); + if (!name) { + return reject(new Error('Failed to create temp file')); + } + resolve({ + filePath: name, + dispose: removeCallback, }); }); } diff --git a/src/client/common/platform/pathUtils.ts b/src/client/common/platform/pathUtils.ts index 6d3a5522db8e..b3be39f4644b 100644 --- a/src/client/common/platform/pathUtils.ts +++ b/src/client/common/platform/pathUtils.ts @@ -1,4 +1,3 @@ -// tslint:disable-next-line:no-suspicious-comment // TODO: Drop this file. // See https://github.com/microsoft/vscode-python/issues/8542. @@ -7,15 +6,14 @@ 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'); +import { untildify } from '../helpers'; @injectable() export class PathUtils implements IPathUtils { private readonly utils: FileSystemPathUtils; constructor( // "true" if targeting a Windows host. - @inject(IsWindows) isWindows: boolean + @inject(IsWindows) isWindows: boolean, ) { const osType = isWindows ? OSType.Windows : OSType.Unknown; // We cannot just use FileSystemPathUtils.withDefaults() because @@ -24,7 +22,7 @@ export class PathUtils implements IPathUtils { untildify('~'), FileSystemPaths.withDefaults(), new Executables(path.delimiter, osType), - path + path, ); } @@ -40,10 +38,8 @@ export class PathUtils implements IPathUtils { 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; } diff --git a/src/client/common/platform/platformService.ts b/src/client/common/platform/platformService.ts index 81706960e6a6..dc9b04cc652c 100644 --- a/src/client/common/platform/platformService.ts +++ b/src/client/common/platform/platformService.ts @@ -1,34 +1,30 @@ // 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 { getSearchPathEnvVarNames } from '../utils/exec'; +import { Architecture, getArchitecture, getOSType, isWindows, OSType } from '../utils/platform'; +import { parseSemVerSafe } from '../utils/version'; 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 pathVariableName(): 'Path' | 'PATH' { + return getSearchPathEnvVarNames(this.osType)[0]; } - public get virtualEnvBinName() { + + public get virtualEnvBinName(): 'Scripts' | 'bin' { return this.isWindows ? 'Scripts' : 'bin'; } + public async getVersion(): Promise { if (this.version) { return this.version; @@ -42,38 +38,38 @@ export class PlatformService implements IPlatformService { try { const ver = coerce(os.release()); if (ver) { - sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { - osVersion: `${ver.major}.${ver.minor}.${ver.patch}` - }); - return (this.version = ver); + this.version = ver; + return this.version; } throw new Error('Unable to parse version'); } catch (ex) { - sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { - failureType: PlatformErrors.FailedToParseVersion - }); - return parseVersion(os.release()); + return parseSemVerSafe(os.release()); } default: throw new Error('Not Supported'); } } + // eslint-disable-next-line class-methods-use-this public get isWindows(): boolean { - return this.osType === OSType.Windows; + return isWindows(); } + public get isMac(): boolean { return this.osType === OSType.OSX; } + public get isLinux(): boolean { return this.osType === OSType.Linux; } + + // eslint-disable-next-line class-methods-use-this public get osRelease(): string { return os.release(); } + + // eslint-disable-next-line class-methods-use-this public get is64bit(): boolean { - // tslint:disable-next-line:no-require-imports - const arch = require('arch'); - return arch() === 'x64'; + return getArchitecture() === Architecture.x64; } } diff --git a/src/client/common/platform/registry.ts b/src/client/common/platform/registry.ts index 6657d8dd8670..f1978cfa6dda 100644 --- a/src/client/common/platform/registry.ts +++ b/src/client/common/platform/registry.ts @@ -1,20 +1,29 @@ import { injectable } from 'inversify'; import { Options } from 'winreg'; +import { traceError } from '../../logging'; import { Architecture } from '../utils/platform'; import { IRegistry, RegistryHive } from './types'; enum RegistryArchitectures { x86 = 'x86', - x64 = 'x64' + 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 }); + 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); + 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; + }, + ); } } @@ -30,7 +39,6 @@ export function getArchitectureDisplayName(arch?: Architecture) { } 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) => { @@ -43,7 +51,6 @@ async function getRegistryValue(options: Options, name: string = '') { } 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) => { @@ -66,7 +73,6 @@ function translateArchitecture(arch?: Architecture): RegistryArchitectures | und } } 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: diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index 1f533a0f80c2..11edc9ada0aa 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -7,12 +7,18 @@ import { SemVer } from 'semver'; import * as vscode from 'vscode'; import { Architecture, OSType } from '../utils/platform'; -//=========================== +// We could use FileType from utils/filesystem.ts, but it's simpler this way. +export import FileType = vscode.FileType; +export import FileStat = vscode.FileStat; +export type ReadStream = fs.ReadStream; +export type WriteStream = fs.WriteStream; + +//= ========================== // registry export enum RegistryHive { HKCU, - HKLM + HKLM, } export const IRegistry = Symbol('IRegistry'); @@ -21,11 +27,9 @@ export interface IRegistry { 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; @@ -41,17 +45,16 @@ export interface IPlatformService { 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. @@ -87,16 +90,12 @@ export interface IFileSystemPathUtils { 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 { + pathExists(filename: string): Promise; // Get information about a file (resolve symlinks). stat(filename: string): Promise; // Get information about a file (do not resolve synlinks). @@ -106,7 +105,7 @@ export interface IRawFileSystem { // 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. @@ -114,7 +113,7 @@ export interface IRawFileSystem { // 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; + writeText(filename: string, data: string | Buffer): Promise; // Write the given text to the end of the file (UTF-8 encoded). appendText(filename: string, text: string): Promise; // Copy a file. @@ -122,7 +121,7 @@ export interface IRawFileSystem { // Delete a file. rmfile(filename: string): Promise; - //*********************** + //* ********************** // directories // Create the directory and any missing parent directories. @@ -134,7 +133,7 @@ export interface IRawFileSystem { // Return the contents of the directory. listdir(dirname: string): Promise<[string, FileType][]>; - //*********************** + //* ********************** // not async // Get information about a file (resolve symlinks). @@ -154,14 +153,14 @@ export interface IFileSystemUtils { 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. @@ -183,13 +182,12 @@ export interface IFileSystemUtils { // 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. @@ -219,6 +217,7 @@ export interface IFileSystem { createWriteStream(path: string): fs.WriteStream; // utils + pathExists(path: string): Promise; fileExists(path: string): Promise; fileExistsSync(path: string): boolean; directoryExists(path: string): Promise; diff --git a/src/client/common/process/baseDaemon.ts b/src/client/common/process/baseDaemon.ts deleted file mode 100644 index abd93668115d..000000000000 --- a/src/client/common/process/baseDaemon.ts +++ /dev/null @@ -1,377 +0,0 @@ -// 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/currentProcess.ts b/src/client/common/process/currentProcess.ts index 8d918f83f26c..b80c32e97b7c 100644 --- a/src/client/common/process/currentProcess.ts +++ b/src/client/common/process/currentProcess.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-any - import { injectable } from 'inversify'; import { ICurrentProcess } from '../types'; import { EnvironmentVariables } from '../variables/types'; diff --git a/src/client/common/process/decoder.ts b/src/client/common/process/decoder.ts index 4e03b48501d0..76cc7a349816 100644 --- a/src/client/common/process/decoder.ts +++ b/src/client/common/process/decoder.ts @@ -2,14 +2,9 @@ // 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); - } +export function decodeBuffer(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 index 86123367f852..377c6580bfd5 100644 --- a/src/client/common/process/internal/python.ts +++ b/src/client/common/process/internal/python.ts @@ -1,8 +1,6 @@ // 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 @@ -15,49 +13,22 @@ import { _ISOLATED as ISOLATED } from './scripts'; // 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); - } +export function execCode(code: string): string[] { + let args = ['-c', code]; // "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[] { +export function execModule(name: string, moduleArgs: string[]): string[] { const args = ['-m', name, ...moduleArgs]; - if (isolated) { - args[0] = ISOLATED; // replace - } // "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)']; + const args = ['-c', 'import sys;print(sys.executable)']; function parse(out: string): string { return out.trim(); @@ -67,14 +38,10 @@ export function getExecutable(): [string[], (out: string) => string] { } 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())' - ]; + // On windows we also need the libs path (second item will + // return c:\xxx\lib\site-packages). This is returned by + // the following: get_python_lib + const args = ['-c', 'from distutils.sysconfig import get_python_lib; print(get_python_lib())']; function parse(out: string): string { return out.trim(); @@ -84,7 +51,7 @@ export function getSitePackages(): [string[], (out: string) => string] { } export function getUserSitePackages(): [string[], (out: string) => string] { - const args = [ISOLATED, 'site', '--user-site']; + const args = ['site', '--user-site']; function parse(out: string): string { return out.trim(); @@ -94,7 +61,6 @@ export function getUserSitePackages(): [string[], (out: string) => string] { } export function isValid(): [string[], (out: string) => boolean] { - // There is no need to isolate this. const args = ['-c', 'print(1234)']; function parse(out: string): boolean { @@ -105,7 +71,7 @@ export function isValid(): [string[], (out: string) => boolean] { } export function isModuleInstalled(name: string): [string[], (out: string) => boolean] { - const args = [ISOLATED, '-c', `import ${name}`]; + const args = ['-c', `import ${name}`]; function parse(_out: string): boolean { // If the command did not fail then the module is installed. @@ -116,7 +82,7 @@ export function isModuleInstalled(name: string): [string[], (out: string) => boo } export function getModuleVersion(name: string): [string[], (out: string) => string] { - const args = [ISOLATED, name, '--version']; + const args = ['-c', `import ${name}; print(${name}.__version__)`]; function parse(out: string): string { return out.trim(); diff --git a/src/client/common/process/internal/scripts/constants.ts b/src/client/common/process/internal/scripts/constants.ts new file mode 100644 index 000000000000..6954592ed3dd --- /dev/null +++ b/src/client/common/process/internal/scripts/constants.ts @@ -0,0 +1,8 @@ +// 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, 'python_files'); diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index ec59c6f6a483..f2c905c02889 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -2,16 +2,12 @@ // Licensed under the MIT License. import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { _SCRIPTS_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 +// the top-level "python_files" 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 @@ -22,203 +18,48 @@ const ISOLATED = _ISOLATED; // 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". +// Some of the scripts are located in subdirectories of "python_files". // 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'; +export * as testingTools from './testing_tools'; -//============================ // interpreterInfo.py type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; type PythonVersionInfo = [number, number, number, ReleaseLevel, number]; -export type PythonEnvInfo = { +export type InterpreterInfoJson = { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean; }; -export function interpreterInfo(): [string[], (out: string) => PythonEnvInfo] { +export const OUTPUT_MARKER_SCRIPT = path.join(_SCRIPTS_DIR, 'get_output_via_markers.py'); + +export function interpreterInfo(): [string[], (out: string) => InterpreterInfoJson] { const script = path.join(SCRIPTS_DIR, 'interpreterInfo.py'); - const args = [ISOLATED, script]; + const args = [script]; - function parse(out: string): PythonEnvInfo { - let json: PythonEnvInfo; + function parse(out: string): InterpreterInfoJson { try { - json = JSON.parse(out); + return 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; - }; -} +// normalizeSelection.py -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]; +export function normalizeSelection(): [string[], (out: string) => string] { + const script = path.join(SCRIPTS_DIR, 'normalizeSelection.py'); + const args = [script]; function parse(out: string) { // The text will be used as-is. @@ -228,54 +69,11 @@ export function normalizeForInterpreter(code: string): [string[], (out: string) 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]; + const script = path.join(SCRIPTS_DIR, 'printEnvVariables.py').fileToCommandArgumentForPythonExt(); + const args = [script]; function parse(out: string): NodeJS.ProcessEnv { return JSON.parse(out); @@ -284,52 +82,79 @@ export function printEnvVariables(): [string[], (out: string) => NodeJS.ProcessE 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 +// eslint-disable-next-line camelcase 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, script, - command.fileToCommandArgument(), + command.fileToCommandArgumentForPythonExt(), // The shell args must come after the command // but before the lockfile. ...shellArgs, - lockfile.fileToCommandArgument() + lockfile.fileToCommandArgumentForPythonExt(), ]; } -//============================ // 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]; + return [script, ...testArgs]; +} + +// run_pytest_script.py +export function pytestlauncher(testArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'vscode_pytest', 'run_pytest_script.py'); + // There is no output to parse, so we do not return a function. + return [script, ...testArgs]; } -//============================ // visualstudio_py_testlauncher.py +// eslint-disable-next-line camelcase 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]; } + +// execution.py +// eslint-disable-next-line camelcase +export function execution_py_testlauncher(testArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'unittestadapter', 'execution.py'); + return [script, ...testArgs]; +} + +// tensorboard_launcher.py + +export function tensorboardLauncher(args: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'tensorboard_launcher.py'); + return [script, ...args]; +} + +// linter.py + +export function linterScript(): string { + const script = path.join(SCRIPTS_DIR, 'linter.py'); + return script; +} + +export function createVenvScript(): string { + const script = path.join(SCRIPTS_DIR, 'create_venv.py'); + return script; +} + +export function createCondaScript(): string { + const script = path.join(SCRIPTS_DIR, 'create_conda.py'); + return script; +} + +export function installedCheckScript(): string { + const script = path.join(SCRIPTS_DIR, 'installed_check.py'); + return script; +} diff --git a/src/client/common/process/internal/scripts/testing_tools.ts b/src/client/common/process/internal/scripts/testing_tools.ts index d2047784622e..60dd21b698b6 100644 --- a/src/client/common/process/internal/scripts/testing_tools.ts +++ b/src/client/common/process/internal/scripts/testing_tools.ts @@ -2,59 +2,19 @@ // Licensed under the MIT License. import * as path from 'path'; -import { _SCRIPTS_DIR } from './index'; +import { _SCRIPTS_DIR } from './constants'; 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[]] { +export function runAdapter(adapterArgs: string[]): string[] { 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 [script, ...adapterArgs]; +} - return [args, parse]; +export function unittestDiscovery(args: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'unittest_discovery.py'); + return [script, ...args]; } diff --git a/src/client/common/process/internal/scripts/vscode_datascience_helpers.ts b/src/client/common/process/internal/scripts/vscode_datascience_helpers.ts deleted file mode 100644 index 880d46bd6738..000000000000 --- a/src/client/common/process/internal/scripts/vscode_datascience_helpers.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 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 index a17896fdd467..b65da8dc81e5 100644 --- a/src/client/common/process/logger.ts +++ b/src/client/common/process/logger.ts @@ -3,43 +3,94 @@ '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 { inject, injectable } from 'inversify'; +import { traceLog } from '../../logging'; +import { IWorkspaceService } from '../application/types'; +import { isCI, isTestExecution } from '../constants'; +import { getOSType, getUserHomeDir, OSType } from '../utils/platform'; import { IProcessLogger, SpawnOptions } from './types'; +import { escapeRegExp } from 'lodash'; +import { replaceAll } from '../stringUtils'; +import { identifyShellFromShellPath } from '../terminal/shellDetectors/baseShellDetector'; +import '../../common/extensions'; @injectable() export class ProcessLogger implements IProcessLogger { - constructor( - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel, - @inject(IPathUtils) private readonly pathUtils: IPathUtils - ) {} + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} - public logProcess(file: string, args: string[], options?: SpawnOptions) { + public logProcess(fileOrCommand: 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)}`); + let command = args + ? [fileOrCommand, ...args].map((e) => e.trimQuotes().toCommandArgumentForPythonExt()).join(' ') + : fileOrCommand; + const info = [`> ${this.getDisplayCommands(command)}`]; + if (options?.cwd) { + const cwd: string = typeof options?.cwd === 'string' ? options?.cwd : options?.cwd?.toString(); + info.push(`cwd: ${this.getDisplayCommands(cwd)}`); + } + if (typeof options?.shell === 'string') { + info.push(`shell: ${identifyShellFromShellPath(options?.shell)}`); } info.forEach((line) => { - traceInfo(line); - this.outputChannel.appendLine(line); + traceLog(line); }); } + + /** + * Formats command strings for display by replacing common paths with symbols. + * - Replaces the workspace folder path with '.' if there's exactly one workspace folder + * - Replaces the user's home directory path with '~' + * @param command The command string to format + * @returns The formatted command string with paths replaced by symbols + */ + private getDisplayCommands(command: string): string { + if (this.workspaceService.workspaceFolders && this.workspaceService.workspaceFolders.length === 1) { + command = replaceMatchesWithCharacter(command, this.workspaceService.workspaceFolders[0].uri.fsPath, '.'); + } + const home = getUserHomeDir(); + if (home) { + command = replaceMatchesWithCharacter(command, home, '~'); + } + return command; + } +} + +/** + * Finds case insensitive matches in the original string and replaces it with character provided. + */ +function replaceMatchesWithCharacter(original: string, match: string, character: string): string { + // Backslashes, plus signs, brackets and other characters have special meaning in regexes, + // we need to escape using an extra backlash so it's not considered special. + function getRegex(match: string) { + let pattern = escapeRegExp(match); + if (getOSType() === OSType.Windows) { + // Match both forward and backward slash versions of 'match' for Windows. + pattern = replaceAll(pattern, '\\\\', '(\\\\|/)'); + } + let regex = new RegExp(pattern, 'ig'); + return regex; + } + + function isPrevioustoMatchRegexALetter(chunk: string, index: number) { + return chunk[index].match(/[a-z]/); + } + + let chunked = original.split(' '); + + for (let i = 0; i < chunked.length; i++) { + let regex = getRegex(match); + const regexResult = regex.exec(chunked[i]); + if (regexResult) { + const regexIndex = regexResult.index; + if (regexIndex > 0 && isPrevioustoMatchRegexALetter(chunked[i], regexIndex - 1)) + regex = getRegex(match.substring(1)); + chunked[i] = chunked[i].replace(regex, character); + } + } + return chunked.join(' '); } diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts index 54cf2f53bf43..4a5aa984fa44 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -1,30 +1,21 @@ // 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 { traceError } from '../../logging'; 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'; +import { execObservable, killPid, plainExec, shellExec } from './rawProcessApis'; +import { ExecutionResult, IProcessService, ObservableExecutionResult, ShellOptions, SpawnOptions } from './types'; +import { workerPlainExec, workerShellExec } from './worker/rawProcessApiWrapper'; -// tslint:disable:no-any export class ProcessService extends EventEmitter implements IProcessService { private processesToKill = new Set(); - constructor(private readonly decoder: IBufferDecoder, private readonly env?: EnvironmentVariables) { + + constructor(private readonly env?: EnvironmentVariables) { super(); } + public static isAlive(pid: number): boolean { try { process.kill(pid, 0); @@ -33,19 +24,12 @@ export class ProcessService extends EventEmitter implements IProcessService { 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. - } + killPid(pid); } - public dispose() { + + public dispose(): void { this.removeAllListeners(); this.processesToKill.forEach((p) => { try { @@ -57,192 +41,38 @@ export class ProcessService extends EventEmitter implements IProcessService { } 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()); - }); - }); - + const execOptions = { ...options, doNotLog: true }; + const result = execObservable(file, args, execOptions, this.env, this.processesToKill); this.emit('exec', file, args, options); - - return { - proc, - out: output, - dispose: disposable.dispose - }; + return result; } - 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()); - }); + public exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { this.emit('exec', file, args, options); - - return deferred.promise; + if (options.useWorker) { + return workerPlainExec(file, args, options); + } + const execOptions = { ...options, doNotLog: true }; + const promise = plainExec(file, args, execOptions, this.env, this.processesToKill); + return 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 }); + this.emit('exec', command, undefined, options); + if (options.useWorker) { + return workerShellExec(command, options); + } + const disposables = new Set(); + const shellOptions = { ...options, doNotLog: true }; + return shellExec(command, shellOptions, this.env, disposables).finally(() => { + // Ensure the process we started is cleaned up. + disposables.forEach((p) => { + try { + p.dispose(); + } catch { + traceError(`Unable to kill process for ${command}`); } }); - 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 index d6da78721138..40204a640dae 100644 --- a/src/client/common/process/processFactory.ts +++ b/src/client/common/process/processFactory.ts @@ -8,19 +8,20 @@ import { Uri } from 'vscode'; import { IDisposableRegistry } from '../types'; import { IEnvironmentVariablesProvider } from '../variables/types'; import { ProcessService } from './proc'; -import { IBufferDecoder, IProcessLogger, IProcessService, IProcessServiceFactory } from './types'; +import { 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 + @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); + public async create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise { + const customEnvVars = options?.doNotUseCustomEnvs + ? undefined + : await this.envVarsService.getEnvironmentVariables(resource); + const proc: IProcessService = new ProcessService(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 deleted file mode 100644 index b588af7f8053..000000000000 --- a/src/client/common/process/pythonDaemon.ts +++ /dev/null @@ -1,130 +0,0 @@ -// 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 deleted file mode 100644 index a4a450b95539..000000000000 --- a/src/client/common/process/pythonDaemonFactory.ts +++ /dev/null @@ -1,134 +0,0 @@ -// 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 deleted file mode 100644 index e82d52f4828d..000000000000 --- a/src/client/common/process/pythonDaemonPool.ts +++ /dev/null @@ -1,266 +0,0 @@ -// 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 index 2f3a86c953cb..cbf898ac5f50 100644 --- a/src/client/common/process/pythonEnvironment.ts +++ b/src/client/common/process/pythonEnvironment.ts @@ -1,17 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CondaEnvironmentInfo } from '../../pythonEnvironments/discovery/locators/services/conda'; +import * as path from 'path'; +import { traceError, traceVerbose } from '../../logging'; +import { Conda, CondaEnvironmentInfo } from '../../pythonEnvironments/common/environmentManagers/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 { isTestExecution } from '../constants'; import { IFileSystem } from '../platform/types'; import * as internalPython from './internal/python'; -import { ExecutionResult, IProcessService, ShellOptions, SpawnOptions } from './types'; +import { ExecutionResult, IProcessService, IPythonEnvironment, ShellOptions, SpawnOptions } from './types'; +import { PixiEnvironmentInfo } from '../../pythonEnvironments/common/environmentManagers/pixi'; -class PythonEnvironment { +const cachedExecutablePath: Map> = new Map>(); + +class PythonEnvironment implements IPythonEnvironment { private cachedInterpreterInformation: InterpreterInformation | undefined | null = null; constructor( @@ -23,17 +28,17 @@ class PythonEnvironment { isValidExecutable(python: string): Promise; // from ProcessService: exec(file: string, args: string[]): Promise>; - shellExec(command: string, timeout: number): Promise>; - } + shellExec(command: string, options?: ShellOptions): Promise>; + }, ) {} - public getExecutionInfo(pythonArgs: string[] = []): PythonExecInfo { + public getExecutionInfo(pythonArgs: string[] = [], pythonExecutable?: string): PythonExecInfo { const python = this.deps.getPythonArgv(this.pythonPath); - return buildPythonExecInfo(python, pythonArgs); + return buildPythonExecInfo(python, pythonArgs, pythonExecutable); } - public getExecutionObservableInfo(pythonArgs: string[] = []): PythonExecInfo { + public getExecutionObservableInfo(pythonArgs: string[] = [], pythonExecutable?: string): PythonExecInfo { const python = this.deps.getObservablePythonArgv(this.pythonPath); - return buildPythonExecInfo(python, pythonArgs); + return buildPythonExecInfo(python, pythonArgs, pythonExecutable); } public async getInterpreterInformation(): Promise { @@ -43,14 +48,34 @@ class PythonEnvironment { return this.cachedInterpreterInformation; } - public async getExecutablePath(): Promise { + 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 result = cachedExecutablePath.get(this.pythonPath); + if (result !== undefined && !isTestExecution()) { + // Another call for this environment has already been made, return its result + return result; + } const python = this.getExecutionInfo(); - return getExecutablePath(python, this.deps.exec); + const promise = getExecutablePath(python, this.deps.shellExec); + cachedExecutablePath.set(this.pythonPath, promise); + return promise; + } + + public async getModuleVersion(moduleName: string): Promise { + const [args, parse] = internalPython.getModuleVersion(moduleName); + const info = this.getExecutionInfo(args); + let data: ExecutionResult; + try { + data = await this.deps.exec(info.command, info.args); + } catch (ex) { + traceVerbose(`Error when getting version of module ${moduleName}`, ex); + return undefined; + } + return parse(data.stdout); } public async isModuleInstalled(moduleName: string): Promise { @@ -59,7 +84,8 @@ class PythonEnvironment { const info = this.getExecutionInfo(args); try { await this.deps.exec(info.command, info.args); - } catch { + } catch (ex) { + traceVerbose(`Error when checking if module is installed ${moduleName}`, ex); return false; } return true; @@ -68,7 +94,7 @@ class PythonEnvironment { private async getInterpreterInformationImpl(): Promise { try { const python = this.getExecutionInfo(); - return await getInterpreterInfo(python, this.deps.shellExec, { info: traceInfo, error: traceError }); + return await getInterpreterInfo(python, this.deps.shellExec, { verbose: traceVerbose, error: traceError }); } catch (ex) { traceError(`Failed to get interpreter information for '${this.pythonPath}'`, ex); } @@ -81,14 +107,25 @@ function createDeps( observablePythonArgv: string[] | undefined, // from ProcessService: exec: (file: string, args: string[], options?: SpawnOptions) => Promise>, - shellExec: (command: string, options?: ShellOptions) => Promise> + shellExec: (command: string, options?: ShellOptions) => Promise>, ) { return { - getPythonArgv: (python: string) => pythonArgv || [python], - getObservablePythonArgv: (python: string) => observablePythonArgv || [python], + getPythonArgv: (python: string) => { + if (path.basename(python) === python) { + // Say when python is `py -3.8` or `conda run python` + pythonArgv = python.split(' '); + } + return pythonArgv || [python]; + }, + getObservablePythonArgv: (python: string) => { + if (path.basename(python) === python) { + observablePythonArgv = python.split(' '); + } + return observablePythonArgv || [python]; + }, isValidExecutable, exec: async (cmd: string, args: string[]) => exec(cmd, args, { throwOnStdErr: true }), - shellExec: async (text: string, timeout: number) => shellExec(text, { timeout }) + shellExec, }; } @@ -96,60 +133,76 @@ export function createPythonEnv( pythonPath: string, // These are used to generate the deps. procs: IProcessService, - fs: IFileSystem + fs: IFileSystem, ): PythonEnvironment { const deps = createDeps( - async (filename) => fs.fileExists(filename), + async (filename) => fs.pathExists(filename), // We use the default: [pythonPath]. undefined, undefined, (file, args, opts) => procs.exec(file, args, opts), - (command, opts) => procs.shellExec(command, opts) + (command, opts) => procs.shellExec(command, opts), ); return new PythonEnvironment(pythonPath, deps); } -export function createCondaEnv( - condaFile: string, +export async function createCondaEnv( 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); + fs: IFileSystem, +): Promise { + const conda = await Conda.getConda(); + const pythonArgv = await conda?.getRunPythonArgs({ name: condaInfo.name, prefix: condaInfo.path }); + if (!pythonArgv) { + return undefined; } - const pythonArgv = [condaFile, ...runArgs, 'python']; const deps = createDeps( - async (filename) => fs.fileExists(filename), + async (filename) => fs.pathExists(filename), + pythonArgv, 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) + (command, opts) => procs.shellExec(command, opts), ); - return new PythonEnvironment(pythonPath, deps); + const interpreterPath = await conda?.getInterpreterPathForEnvironment({ + name: condaInfo.name, + prefix: condaInfo.path, + }); + if (!interpreterPath) { + return undefined; + } + return new PythonEnvironment(interpreterPath, deps); } -export function createWindowsStoreEnv( +export async function createPixiEnv( + pixiEnv: PixiEnvironmentInfo, + // These are used to generate the deps. + procs: IProcessService, + fs: IFileSystem, +): Promise { + const pythonArgv = pixiEnv.pixi.getRunPythonArgs(pixiEnv.manifestPath, pixiEnv.envName); + const deps = createDeps( + async (filename) => fs.pathExists(filename), + pythonArgv, + pythonArgv, + (file, args, opts) => procs.exec(file, args, opts), + (command, opts) => procs.shellExec(command, opts), + ); + return new PythonEnvironment(pixiEnv.interpreterPath, deps); +} + +export function createMicrosoftStoreEnv( pythonPath: string, // These are used to generate the deps. - procs: IProcessService + procs: IProcessService, ): PythonEnvironment { const deps = createDeps( /** - * With windows store python apps, we have generally use the + * With microsoft 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 + * executable using sys.executable for microsoft store python * interpreters. */ async (_f: string) => true, @@ -157,7 +210,7 @@ export function createWindowsStoreEnv( undefined, undefined, (file, args, opts) => procs.exec(file, args, opts), - (command, opts) => procs.shellExec(command, 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 index 81a1bb89d968..efb05c3c9d12 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -1,237 +1,191 @@ // 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 { IActivatedEnvironmentLaunch, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { CondaEnvironmentInfo } from '../../pythonEnvironments/discovery/locators/services/conda'; 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 { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from '../types'; import { ProcessService } from './proc'; -import { PythonDaemonFactory } from './pythonDaemonFactory'; -import { PythonDaemonExecutionServicePool } from './pythonDaemonPool'; -import { createCondaEnv, createPythonEnv, createWindowsStoreEnv } from './pythonEnvironment'; +import { createCondaEnv, createPythonEnv, createMicrosoftStoreEnv, createPixiEnv } from './pythonEnvironment'; import { createPythonProcessService } from './pythonProcess'; import { - DaemonExecutionFactoryCreationOptions, ExecutionFactoryCreateWithEnvironmentOptions, ExecutionFactoryCreationOptions, - IBufferDecoder, IProcessLogger, IProcessService, IProcessServiceFactory, - IPythonDaemonExecutionService, + IPythonEnvironment, 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'; +import { IInterpreterAutoSelectionService } from '../../interpreter/autoSelection/types'; +import { sleep } from '../utils/async'; +import { traceError } from '../../logging'; +import { getPixi, getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi'; @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(IWindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter, - @inject(IPlatformService) private readonly platformService: IPlatformService + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IInterpreterAutoSelectionService) private readonly autoSelection: IInterpreterAutoSelectionService, + @inject(IInterpreterPathService) private readonly interpreterPathExpHelper: IInterpreterPathService, ) { // 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; + let { pythonPath } = options; + if (!pythonPath || pythonPath === 'python') { + const activatedEnvLaunch = this.serviceContainer.get( + IActivatedEnvironmentLaunch, + ); + await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + // If python path wasn't passed in, we need to auto select it and then read it + // from the configuration. + const interpreterPath = this.interpreterPathExpHelper.get(options.resource); + if (!interpreterPath || interpreterPath === 'python') { + // Block on autoselection if no interpreter selected. + // Note autoselection blocks on discovery, so we do not want discovery component + // to block on this code. Discovery component should 'options.pythonPath' before + // calling into this, so this scenario should not happen. But in case consumer + // makes such an error. So break the loop via timeout and log error. + const success = await Promise.race([ + this.autoSelection.autoSelectInterpreter(options.resource).then(() => true), + sleep(50000).then(() => false), + ]); + if (!success) { + traceError( + 'Autoselection timeout out, this is likely a issue with how consumer called execution factory API. Using default python to execute.', + ); + } + } + 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) - ); - } + if (await getPixi()) { + const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); + if (pixiExecutionService) { + return pixiExecutionService; + } + } - 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! as unknown) as T; + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); + if (condaExecutionService) { + return condaExecutionService; } - // 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(); - } - }; + const windowsStoreInterpreterCheck = this.pyenvs.isMicrosoftStoreInterpreter.bind(this.pyenvs); - let promise: Promise; + const env = (await windowsStoreInterpreterCheck(pythonPath)) + ? createMicrosoftStoreEnv(pythonPath, processService) + : createPythonEnv(pythonPath, processService, this.fileSystem); - 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; - }); + return createPythonService(processService, env); } + public async createActivatedEnvironment( - options: ExecutionFactoryCreateWithEnvironmentOptions + options: ExecutionFactoryCreateWithEnvironmentOptions, ): Promise { const envVars = await this.activationHelper.getActivatedEnvironmentVariables( options.resource, options.interpreter, - options.allowEnvironmentFetchExceptions + 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 + 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 }); + const processService: IProcessService = new ProcessService({ ...envVars }); processService.on('exec', this.logger.logProcess.bind(this.logger)); this.disposables.push(processService); - return createPythonService(pythonPath, processService, this.fileSystem); + if (await getPixi()) { + const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService); + if (pixiExecutionService) { + return pixiExecutionService; + } + } + + const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService); + if (condaExecutionService) { + return condaExecutionService; + } + + const env = createPythonEnv(pythonPath, processService, this.fileSystem); + return createPythonService(processService, env); } - // 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 + processService: IProcessService, ): 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] - ); + const condaLocatorService = this.serviceContainer.get(IComponentAdapter); + const [condaEnvironment] = await Promise.all([condaLocatorService.getCondaEnvironment(pythonPath)]); + if (!condaEnvironment) { + return undefined; } + const env = await createCondaEnv(condaEnvironment, processService, this.fileSystem); + if (!env) { + return undefined; + } + return createPythonService(processService, env); + } - return Promise.resolve(undefined); + public async createPixiExecutionService( + pythonPath: string, + processService: IProcessService, + ): Promise { + const pixiEnvironment = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnvironment) { + return undefined; + } + + const env = await createPixiEnv(pixiEnvironment, processService, this.fileSystem); + if (env) { + return createPythonService(processService, env); + } + + return 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); - } +function createPythonService(procService: IProcessService, env: IPythonEnvironment): IPythonExecutionService { const procs = createPythonProcessService(procService, env); return { getInterpreterInformation: () => env.getInterpreterInformation(), getExecutablePath: () => env.getExecutablePath(), isModuleInstalled: (m) => env.isModuleInstalled(m), + getModuleVersion: (m) => env.getModuleVersion(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) + execModule: (m, a, o) => procs.execModule(m, a, o), + execForLinter: (m, a, o) => procs.execForLinter(m, a, o), }; } diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts index b6ce351ee318..f4d1de8883ba 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -5,7 +5,7 @@ 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'; +import { ExecutionResult, IProcessService, IPythonEnvironment, ObservableExecutionResult, SpawnOptions } from './types'; class PythonProcessService { constructor( @@ -18,7 +18,7 @@ class PythonProcessService { // 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 { @@ -30,7 +30,7 @@ class PythonProcessService { public execModuleObservable( moduleName: string, moduleArgs: string[], - options: SpawnOptions + options: SpawnOptions, ): ObservableExecutionResult { const args = internalPython.execModule(moduleName, moduleArgs); const opts: SpawnOptions = { ...options }; @@ -47,7 +47,7 @@ class PythonProcessService { public async execModule( moduleName: string, moduleArgs: string[], - options: SpawnOptions + options: SpawnOptions, ): Promise> { const args = internalPython.execModule(moduleName, moduleArgs); const opts: SpawnOptions = { ...options }; @@ -64,16 +64,32 @@ class PythonProcessService { return result; } + + public async execForLinter( + moduleName: string, + args: string[], + options: SpawnOptions, + ): Promise> { + 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; - } + env: IPythonEnvironment, ) { const deps = { // from PythonService: @@ -82,7 +98,7 @@ export function createPythonProcessService( 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) + 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 index cb1068651c82..136ab56fe0c4 100644 --- a/src/client/common/process/pythonToolService.ts +++ b/src/client/common/process/pythonToolService.ts @@ -11,7 +11,7 @@ import { IPythonExecutionFactory, IPythonToolExecutionService, ObservableExecutionResult, - SpawnOptions + SpawnOptions, } from './types'; @injectable() @@ -20,7 +20,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService { public async execObservable( executionInfo: ExecutionInfo, options: SpawnOptions, - resource: Uri + resource: Uri, ): Promise> { if (options.env) { throw new Error('Environment variables are not supported'); @@ -40,7 +40,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService { public async exec( executionInfo: ExecutionInfo, options: SpawnOptions, - resource: Uri + resource: Uri, ): Promise> { if (options.env) { throw new Error('Environment variables are not supported'); @@ -49,7 +49,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService { const pythonExecutionService = await this.serviceContainer .get(IPythonExecutionFactory) .create({ resource }); - return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options); + return pythonExecutionService.execModule(executionInfo.moduleName, executionInfo.args, options); } else { const processService = await this.serviceContainer .get(IProcessServiceFactory) @@ -57,4 +57,23 @@ export class PythonToolExecutionService implements IPythonToolExecutionService { return processService.exec(executionInfo.execPath!, executionInfo.args, { ...options }); } } + + public async execForLinter( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise> { + if (options.env) { + throw new Error('Environment variables are not supported'); + } + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); + + if (executionInfo.execPath) { + return pythonExecutionService.exec(executionInfo.args, options); + } + + return pythonExecutionService.execForLinter(executionInfo.moduleName!, executionInfo.args, options); + } } diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts new file mode 100644 index 000000000000..864191851c91 --- /dev/null +++ b/src/client/common/process/rawProcessApis.ts @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { exec, execSync, spawn } from 'child_process'; +import { Readable } from 'stream'; +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, ObservableExecutionResult, Output, ShellOptions, SpawnOptions, StdErrError } from './types'; +import { noop } from '../utils/misc'; +import { decodeBuffer } from './decoder'; +import { traceVerbose } from '../../logging'; +import { WorkspaceService } from '../application/workspace'; +import { ProcessLogger } from './logger'; + +const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; + +function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { + const defaultOptions = { ...options }; + const execOptions = defaultOptions as SpawnOptions; + if (execOptions) { + execOptions.encoding = + typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 + ? execOptions.encoding + : DEFAULT_ENCODING; + const { encoding } = execOptions; + delete execOptions.encoding; + execOptions.encoding = encoding; + } + if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { + const env = defaultEnv || 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; +} + +export function shellExec( + command: string, + options: ShellOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const shellOptions = getDefaultOptions(options, defaultEnv); + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + const loggingOptions = { ...shellOptions, encoding: shellOptions.encoding ?? undefined }; + processLogger.logProcess(command, undefined, loggingOptions); + } + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callback = (e: any, stdout: any, stderr: any) => { + if (e && e !== null) { + reject(e); + } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { + reject(new Error(stderr)); + } else { + stdout = filterOutputUsingCondaRunMarkers(stdout); + // 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 }); + } + }; + let procExited = false; + const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + if (disposables) { + disposables.add(disposable); + } + }); +} + +export function plainExec( + file: string, + args: string[], + options: SpawnOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } + const proc = spawn(file, args, spawnOptions); + // Listen to these errors (unhandled errors in streams tears down the process). + // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. + proc.stdout?.on('error', noop); + proc.stderr?.on('error', noop); + const deferred = createDeferred>(); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!proc.killed && !deferred.completed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + disposables?.add(disposable); + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + if (options.token) { + internalDisposables.push(options.token.onCancellationRequested(disposable.dispose)); + } + + const stdoutBuffers: Buffer[] = []; + on(proc.stdout, 'data', (data: Buffer) => { + stdoutBuffers.push(data); + options.outputChannel?.append(data.toString()); + }); + const stderrBuffers: Buffer[] = []; + on(proc.stderr, 'data', (data: Buffer) => { + if (options.mergeStdOutErr) { + stdoutBuffers.push(data); + stderrBuffers.push(data); + } else { + stderrBuffers.push(data); + } + options.outputChannel?.append(data.toString()); + }); + + proc.once('close', () => { + if (deferred.completed) { + return; + } + const stderr: string | undefined = + stderrBuffers.length === 0 ? undefined : decodeBuffer(stderrBuffers, encoding); + if ( + stderr && + stderr.length > 0 && + options.throwOnStdErr && + // ignore this specific error silently; see this issue for context: https://github.com/microsoft/vscode/issues/75932 + !(PS_ERROR_SCREEN_BOGUS.test(stderr) && stderr.replace(PS_ERROR_SCREEN_BOGUS, '').trim().length === 0) + ) { + deferred.reject(new StdErrError(stderr)); + } else { + let stdout = decodeBuffer(stdoutBuffers, encoding); + stdout = filterOutputUsingCondaRunMarkers(stdout); + deferred.resolve({ stdout, stderr }); + } + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + proc.once('error', (ex) => { + deferred.reject(ex); + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + + return deferred.promise; +} + +function filterOutputUsingCondaRunMarkers(stdout: string) { + // These markers are added if conda run is used or `interpreterInfo.py` is + // run, see `get_output_via_markers.py`. + const regex = />>>PYTHON-EXEC-OUTPUT([\s\S]*)<<= 2 ? match[1].trim() : undefined; + return filteredOut !== undefined ? filteredOut : stdout; +} + +function removeCondaRunMarkers(out: string) { + out = out.replace('>>>PYTHON-EXEC-OUTPUT\r\n', '').replace('>>>PYTHON-EXEC-OUTPUT\n', ''); + return out.replace('<<, +): ObservableExecutionResult { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } + const proc = spawn(file, args, spawnOptions); + let procExited = false; + const disposable: IDisposable = { + dispose() { + if (proc && proc.pid && !proc.killed && !procExited) { + killPid(proc.pid); + } + if (proc) { + proc.unref(); + } + }, + }; + disposables?.add(disposable); + + const output = new Observable>((subscriber) => { + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + if (options.token) { + internalDisposables.push( + options.token.onCancellationRequested(() => { + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + procExited = true; + } + }), + ); + } + + const sendOutput = (source: 'stdout' | 'stderr', data: Buffer) => { + let out = decodeBuffer([data], encoding); + if (source === 'stderr' && options.throwOnStdErr) { + subscriber.error(new StdErrError(out)); + } else { + // Because all of output is not retrieved at once, filtering out the + // actual output using markers is not possible. Hence simply remove + // the markers and return original output. + out = removeCondaRunMarkers(out); + subscriber.next({ source, 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(); + internalDisposables.forEach((d) => d.dispose()); + }); + proc.once('exit', () => { + procExited = true; + subscriber.complete(); + internalDisposables.forEach((d) => d.dispose()); + }); + proc.once('error', (ex) => { + procExited = true; + subscriber.error(ex); + internalDisposables.forEach((d) => d.dispose()); + }); + if (options.stdinStr !== undefined) { + proc.stdin?.write(options.stdinStr); + proc.stdin?.end(); + } + }); + + return { + proc, + out: output, + dispose: disposable.dispose, + }; +} + +export function killPid(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`); // NOSONAR + } else { + process.kill(pid); + } + } catch { + traceVerbose('Unable to kill process with pid', pid); + } +} diff --git a/src/client/common/process/serviceRegistry.ts b/src/client/common/process/serviceRegistry.ts index 27684a20cc32..0ea57231148a 100644 --- a/src/client/common/process/serviceRegistry.ts +++ b/src/client/common/process/serviceRegistry.ts @@ -2,14 +2,12 @@ // 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'; +import { 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 index 3a1c8b75e3f0..9263e69cbe21 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -3,18 +3,10 @@ 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 { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonExecInfo } from '../../pythonEnvironments/exec'; -import { InterpreterInformation, PythonInterpreter } from '../../pythonEnvironments/info'; +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'; @@ -26,17 +18,18 @@ export type ObservableExecutionResult = { dispose(): void; }; -// tslint:disable-next-line:interface-name export type SpawnOptions = ChildProcessSpawnOptions & { encoding?: string; token?: CancellationToken; mergeStdOutErr?: boolean; throwOnStdErr?: boolean; extraVariables?: NodeJS.ProcessEnv; + outputChannel?: OutputChannel; + stdinStr?: string; + useWorker?: boolean; }; -// tslint:disable-next-line:interface-name -export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean; useWorker?: boolean }; export type ExecutionResult = { stdout: T; @@ -45,7 +38,12 @@ export type ExecutionResult = { export const IProcessLogger = Symbol('IProcessLogger'); export interface IProcessLogger { - logProcess(file: string, ars: string[], options?: SpawnOptions): void; + /** + * Pass `args` as `undefined` if first argument is supposed to be a shell command. + * Note it is assumed that command args are always quoted and respect + * `String.prototype.toCommandArgument()` prototype. + */ + logProcess(fileOrCommand: string, args?: string[], options?: SpawnOptions): void; } export interface IProcessService extends IDisposable { @@ -58,7 +56,7 @@ export interface IProcessService extends IDisposable { export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); export interface IProcessServiceFactory { - create(resource?: Uri): Promise; + create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise; } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); @@ -66,71 +64,9 @@ 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?: PythonInterpreter; + interpreter?: PythonEnvironment; allowEnvironmentFetchExceptions?: boolean; /** * Ignore running `conda run` when running code. @@ -138,36 +74,22 @@ export type ExecutionFactoryCreateWithEnvironmentOptions = { * * @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 + processService: IProcessService, ): Promise; } export const IPythonExecutionService = Symbol('IPythonExecutionService'); export interface IPythonExecutionService { getInterpreterInformation(): Promise; - getExecutablePath(): Promise; + getExecutablePath(): Promise; isModuleInstalled(moduleName: string): Promise; + getModuleVersion(moduleName: string): Promise; getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult; @@ -175,18 +97,19 @@ export interface IPythonExecutionService { exec(args: string[], options: SpawnOptions): Promise>; execModule(moduleName: string, args: string[], options: SpawnOptions): Promise>; + execForLinter(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 interface IPythonEnvironment { + getInterpreterInformation(): Promise; + getExecutionObservableInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo; + getExecutablePath(): Promise; + isModuleInstalled(moduleName: string): Promise; + getModuleVersion(moduleName: string): Promise; + getExecutionInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo; +} + +export type ShellExecFunc = (command: string, options?: ShellOptions | undefined) => Promise>; export class StdErrError extends Error { constructor(message: string) { @@ -194,17 +117,14 @@ export class StdErrError extends Error { } } -export interface IExecutionEnvironmentVariablesService { - getEnvironmentVariables(resource?: Uri): Promise; -} - export const IPythonToolExecutionService = Symbol('IPythonToolRunnerService'); export interface IPythonToolExecutionService { execObservable( executionInfo: ExecutionInfo, options: SpawnOptions, - resource: Uri + resource: Uri, ): Promise>; exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise>; + execForLinter(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise>; } diff --git a/src/client/common/process/worker/main.ts b/src/client/common/process/worker/main.ts new file mode 100644 index 000000000000..324673618942 --- /dev/null +++ b/src/client/common/process/worker/main.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Worker } from 'worker_threads'; +import * as path from 'path'; +import { traceVerbose, traceError } from '../../../logging/index'; + +/** + * Executes a worker file. Make sure to declare the worker file as a entry in the webpack config. + * @param workerFileName Filename of the worker file to execute, it has to end with ".worker.js" for webpack to bundle it. + * @param workerData Arguments to the worker file. + * @returns + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export async function executeWorkerFile(workerFileName: string, workerData: any): Promise { + if (!workerFileName.endsWith('.worker.js')) { + throw new Error('Worker file must end with ".worker.js" for webpack to bundle webworkers'); + } + return new Promise((resolve, reject) => { + const worker = new Worker(workerFileName, { workerData }); + const id = worker.threadId; + traceVerbose( + `Worker id ${id} for file ${path.basename(workerFileName)} with data ${JSON.stringify(workerData)}`, + ); + worker.on('message', (msg: { err: Error; res: unknown }) => { + if (msg.err) { + reject(msg.err); + } + resolve(msg.res); + }); + worker.on('error', (ex: Error) => { + traceError(`Error in worker ${workerFileName}`, ex); + reject(ex); + }); + worker.on('exit', (code) => { + traceVerbose(`Worker id ${id} exited with code ${code}`); + if (code !== 0) { + reject(new Error(`Worker ${workerFileName} stopped with exit code ${code}`)); + } + }); + }); +} diff --git a/src/client/common/process/worker/plainExec.worker.ts b/src/client/common/process/worker/plainExec.worker.ts new file mode 100644 index 000000000000..f44ea15f9653 --- /dev/null +++ b/src/client/common/process/worker/plainExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerPlainExecImpl } from './workerRawProcessApis'; + +_workerPlainExecImpl(workerData.file, workerData.args, workerData.options) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((err) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ err }); + }); diff --git a/src/client/common/process/worker/rawProcessApiWrapper.ts b/src/client/common/process/worker/rawProcessApiWrapper.ts new file mode 100644 index 000000000000..e6476df5d8fa --- /dev/null +++ b/src/client/common/process/worker/rawProcessApiWrapper.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { SpawnOptions } from 'child_process'; +import * as path from 'path'; +import { executeWorkerFile } from './main'; +import { ExecutionResult, ShellOptions } from './types'; + +export function workerShellExec(command: string, options: ShellOptions): Promise> { + return executeWorkerFile(path.join(__dirname, 'shellExec.worker.js'), { + command, + options, + }); +} + +export function workerPlainExec( + file: string, + args: string[], + options: SpawnOptions = {}, +): Promise> { + return executeWorkerFile(path.join(__dirname, 'plainExec.worker.js'), { + file, + args, + options, + }); +} diff --git a/src/client/common/process/worker/shellExec.worker.ts b/src/client/common/process/worker/shellExec.worker.ts new file mode 100644 index 000000000000..f4e9809a29a5 --- /dev/null +++ b/src/client/common/process/worker/shellExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerShellExecImpl } from './workerRawProcessApis'; + +_workerShellExecImpl(workerData.command, workerData.options, workerData.defaultEnv) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((ex) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ ex }); + }); diff --git a/src/client/common/process/worker/types.ts b/src/client/common/process/worker/types.ts new file mode 100644 index 000000000000..5c58aec10214 --- /dev/null +++ b/src/client/common/process/worker/types.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; + +export function noop() {} +export interface IDisposable { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispose(): void | undefined | Promise; +} +export type EnvironmentVariables = Record; +export class StdErrError extends Error { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(message: string) { + super(message); + } +} + +export type SpawnOptions = ChildProcessSpawnOptions & { + encoding?: string; + // /** + // * Can't use `CancellationToken` here as it comes from vscode which is not available in worker threads. + // */ + // token?: CancellationToken; + mergeStdOutErr?: boolean; + throwOnStdErr?: boolean; + extraVariables?: NodeJS.ProcessEnv; + // /** + // * Can't use `OutputChannel` here as it comes from vscode which is not available in worker threads. + // */ + // outputChannel?: OutputChannel; + stdinStr?: string; +}; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; + +export type ExecutionResult = { + stdout: T; + stderr?: T; +}; diff --git a/src/client/common/process/worker/workerRawProcessApis.ts b/src/client/common/process/worker/workerRawProcessApis.ts new file mode 100644 index 000000000000..cfae9b1e6471 --- /dev/null +++ b/src/client/common/process/worker/workerRawProcessApis.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// !!!! IMPORTANT: DO NOT IMPORT FROM VSCODE MODULE AS IT IS NOT AVAILABLE INSIDE WORKER THREADS !!!! + +import { exec, execSync, spawn } from 'child_process'; +import { Readable } from 'stream'; +import { createDeferred } from '../../utils/async'; +import { DEFAULT_ENCODING } from '../constants'; +import { decodeBuffer } from '../decoder'; +import { + ShellOptions, + SpawnOptions, + EnvironmentVariables, + IDisposable, + noop, + StdErrError, + ExecutionResult, +} from './types'; +import { traceWarn } from '../../../logging'; + +const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; + +function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { + const defaultOptions = { ...options }; + const execOptions = defaultOptions as SpawnOptions; + if (execOptions) { + execOptions.encoding = + typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 + ? execOptions.encoding + : DEFAULT_ENCODING; + const { encoding } = execOptions; + delete execOptions.encoding; + execOptions.encoding = encoding; + } + if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { + const env = defaultEnv || 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; +} + +export function _workerShellExecImpl( + command: string, + options: ShellOptions, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const shellOptions = getDefaultOptions(options, defaultEnv); + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callback = (e: any, stdout: any, stderr: any) => { + if (e && e !== null) { + reject(e); + } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { + reject(new Error(stderr)); + } else { + stdout = filterOutputUsingCondaRunMarkers(stdout); + // 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 }); + } + }; + let procExited = false; + const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + if (disposables) { + disposables.add(disposable); + } + }); +} + +export function _workerPlainExecImpl( + file: string, + args: string[], + options: SpawnOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + const proc = spawn(file, args, spawnOptions); + // Listen to these errors (unhandled errors in streams tears down the process). + // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. + proc.stdout?.on('error', noop); + proc.stderr?.on('error', noop); + const deferred = createDeferred>(); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!proc.killed && !deferred.completed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + disposables?.add(disposable); + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + // Tokens not supported yet as they come from vscode module which is not available. + // if (options.token) { + // internalDisposables.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 : decodeBuffer(stderrBuffers, encoding); + if ( + stderr && + stderr.length > 0 && + options.throwOnStdErr && + // ignore this specific error silently; see this issue for context: https://github.com/microsoft/vscode/issues/75932 + !(PS_ERROR_SCREEN_BOGUS.test(stderr) && stderr.replace(PS_ERROR_SCREEN_BOGUS, '').trim().length === 0) + ) { + deferred.reject(new StdErrError(stderr)); + } else { + let stdout = decodeBuffer(stdoutBuffers, encoding); + stdout = filterOutputUsingCondaRunMarkers(stdout); + deferred.resolve({ stdout, stderr }); + } + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + proc.once('error', (ex) => { + deferred.reject(ex); + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + + return deferred.promise; +} + +function filterOutputUsingCondaRunMarkers(stdout: string) { + // These markers are added if conda run is used or `interpreterInfo.py` is + // run, see `get_output_via_markers.py`. + const regex = />>>PYTHON-EXEC-OUTPUT([\s\S]*)<<= 2 ? match[1].trim() : undefined; + return filteredOut !== undefined ? filteredOut : stdout; +} + +function killPid(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`); // NOSONAR + } else { + process.kill(pid); + } + } catch { + traceWarn('Unable to kill process with pid', pid); + } +} diff --git a/src/client/common/refBool.ts b/src/client/common/refBool.ts deleted file mode 100644 index c0ef43a20777..000000000000 --- a/src/client/common/refBool.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 index 444b666c70e3..abd2b220e400 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -1,10 +1,23 @@ // 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 { + IBrowserService, + IConfigurationService, + ICurrentProcess, + IExperimentService, + IExtensions, + IInstaller, + IInterpreterPathService, + IPathUtils, + IPersistentStateFactory, + IRandom, + IToolExecutionPath, + IsWindows, + ToolExecutionPath, +} from './types'; import { IServiceManager } from '../ioc/types'; +import { JupyterExtensionDependencyManager } from '../jupyter/jupyterExtensionDependencyManager'; import { ImportTracker } from '../telemetry/importTracker'; import { IImportTracker } from '../telemetry/types'; import { ActiveResourceService } from './application/activeResource'; @@ -13,13 +26,11 @@ 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 { ReportIssueCommandHandler } from './application/commands/reportIssueCommand'; 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, @@ -27,46 +38,22 @@ import { IApplicationShell, IClipboard, ICommandManager, - ICustomEditorService, + IContextKeyManager, IDebugService, IDocumentManager, + IJupyterExtensionDependencyManager, ILanguageService, - ILiveShareApi, ITerminalManager, - IVSCodeNotebook, - IWorkspaceService + 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 { PipEnvExecutionPath } from './configuration/executionSettings/pipEnvExecution'; 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'; @@ -74,6 +61,7 @@ import { IProcessLogger } from './process/types'; import { TerminalActivator } from './terminal/activator'; import { PowershellTerminalActivationFailedHandler } from './terminal/activator/powershellFailedHandler'; import { Bash } from './terminal/environmentActivationProviders/bash'; +import { Nushell } from './terminal/environmentActivationProviders/nushell'; import { CommandPromptAndPowerShell } from './terminal/environmentActivationProviders/commandPrompt'; import { CondaActivationCommandProvider } from './terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pipEnvActivationProvider'; @@ -91,44 +79,46 @@ import { ITerminalActivator, ITerminalHelper, ITerminalServiceFactory, - TerminalActivationProviders + 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'; +import { ContextKeyManager } from './application/contextKeyManager'; +import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile'; +import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt'; +import { isWindows } from './utils/platform'; +import { PixiActivationCommandProvider } from './terminal/environmentActivationProviders/pixiActivationProvider'; -// tslint:disable-next-line: max-func-body-length -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingletonInstance(IsWindows, isWindows()); serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); serviceManager.addSingleton(IExtensions, Extensions); serviceManager.addSingleton(IRandom, Random); serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); + serviceManager.addBinding(IPersistentStateFactory, IExtensionSingleActivationService); 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( + IJupyterExtensionDependencyManager, + JupyterExtensionDependencyManager, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + RequireJupyterPrompt, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + CreatePythonFileCommandHandler, + ); serviceManager.addSingleton(ICommandManager, CommandManager); + serviceManager.addSingleton(IContextKeyManager, ContextKeyManager); serviceManager.addSingleton(IConfigurationService, ConfigurationService); serviceManager.addSingleton(IWorkspaceService, WorkspaceService); serviceManager.addSingleton(IProcessLogger, ProcessLogger); @@ -138,85 +128,64 @@ export function registerTypes(serviceManager: IServiceManager) { 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 + 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 + TerminalActivationProviders.bashCShellFish, ); serviceManager.addSingleton( ITerminalActivationCommandProvider, CommandPromptAndPowerShell, - TerminalActivationProviders.commandPromptAndPowerShell + TerminalActivationProviders.commandPromptAndPowerShell, + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, ); serviceManager.addSingleton( ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, - TerminalActivationProviders.pyenv + TerminalActivationProviders.pyenv, ); serviceManager.addSingleton( ITerminalActivationCommandProvider, CondaActivationCommandProvider, - TerminalActivationProviders.conda + TerminalActivationProviders.conda, + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PixiActivationCommandProvider, + TerminalActivationProviders.pixi, ); serviceManager.addSingleton( ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, - TerminalActivationProviders.pipenv + TerminalActivationProviders.pipenv, ); - serviceManager.addSingleton(IFeatureDeprecationManager, FeatureDeprecationManager); + serviceManager.addSingleton(IToolExecutionPath, PipEnvExecutionPath, ToolExecutionPath.pipenv); - 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 + ReloadVSCodeCommandHandler, ); serviceManager.addSingleton( IExtensionSingleActivationService, - DebugSessionTelemetry + ReportIssueCommandHandler, ); - serviceManager.addSingleton(ICustomEditorService, CustomEditorService); } diff --git a/src/client/common/startPage/startPage.ts b/src/client/common/startPage/startPage.ts deleted file mode 100644 index 349195e06ef9..000000000000 --- a/src/client/common/startPage/startPage.ts +++ /dev/null @@ -1,306 +0,0 @@ -// 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 { WebViewHost } from '../../datascience/webViewHost'; -import { sendTelemetryEvent } from '../../telemetry'; -import { - IApplicationEnvironment, - IApplicationShell, - ICommandManager, - IDocumentManager, - IWebPanelProvider, - 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 WebViewHost 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(IWebPanelProvider) provider: IWebPanelProvider, - @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 deleted file mode 100644 index 189ec718b04b..000000000000 --- a/src/client/common/startPage/startPageMessageListener.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { IWebPanel, IWebPanelMessageListener } 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 IWebPanelMessageListener { - private disposedCallback: () => void; - private callback: (message: string, payload: any) => void; - private viewChanged: (panel: IWebPanel) => void; - - constructor( - callback: (message: string, payload: any) => void, - viewChanged: (panel: IWebPanel) => 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: IWebPanel) { - // Forward this onto our callback - this.viewChanged(panel); - } -} diff --git a/src/client/common/startPage/types.ts b/src/client/common/startPage/types.ts deleted file mode 100644 index a147085fe426..000000000000 --- a/src/client/common/startPage/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -// 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/stringUtils.ts b/src/client/common/stringUtils.ts new file mode 100644 index 000000000000..02ca51082ea8 --- /dev/null +++ b/src/client/common/stringUtils.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface SplitLinesOptions { + trim?: boolean; + removeEmptyEntries?: boolean; +} + +/** + * 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. + */ +export function splitLines( + source: string, + splitOptions: SplitLinesOptions = { removeEmptyEntries: true, trim: true }, +): string[] { + let lines = source.split(/\r?\n/g); + if (splitOptions?.trim) { + lines = lines.map((line) => line.trim()); + } + if (splitOptions?.removeEmptyEntries) { + lines = lines.filter((line) => line.length > 0); + } + return lines; +} + +/** + * Replaces all instances of a substring with a new substring. + */ +export function replaceAll(source: string, substr: string, newSubstr: string): string { + if (!source) { + return source; + } + + /** Escaping function from the MDN web docs site + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + * Escapes all the following special characters in a string . * + ? ^ $ { } ( ) | \ \\ + */ + + function escapeRegExp(unescapedStr: string): string { + return unescapedStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + } + + return source.replace(new RegExp(escapeRegExp(substr), 'g'), newSubstr); +} diff --git a/src/client/common/terminal/activator/base.ts b/src/client/common/terminal/activator/base.ts index 27ad3d6ad5d5..b4d2f888d5d2 100644 --- a/src/client/common/terminal/activator/base.ts +++ b/src/client/common/terminal/activator/base.ts @@ -4,6 +4,7 @@ 'use strict'; import { Terminal } from 'vscode'; +import { traceVerbose } from '../../../logging'; import { createDeferred, sleep } from '../../utils/async'; import { ITerminalActivator, ITerminalHelper, TerminalActivationOptions, TerminalShellType } from '../types'; @@ -12,7 +13,7 @@ export class BaseTerminalActivator implements ITerminalActivator { constructor(private readonly helper: ITerminalHelper) {} public async activateEnvironmentInTerminal( terminal: Terminal, - options?: TerminalActivationOptions + options?: TerminalActivationOptions, ): Promise { if (this.activatedTerminals.has(terminal)) { return this.activatedTerminals.get(terminal)!; @@ -24,12 +25,13 @@ export class BaseTerminalActivator implements ITerminalActivator { const activationCommands = await this.helper.getEnvironmentActivationCommands( terminalShellType, options?.resource, - options?.interpreter + options?.interpreter, ); let activated = false; if (activationCommands) { - for (const command of activationCommands!) { + for (const command of activationCommands) { terminal.show(options?.preserveFocus); + traceVerbose(`Command sent to terminal: ${command}`); terminal.sendText(command); await this.waitForCommandToProcess(terminalShellType); activated = true; diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts index b7b8beb147a6..cde04bdbf10d 100644 --- a/src/client/common/terminal/activator/index.ts +++ b/src/client/common/terminal/activator/index.ts @@ -5,27 +5,49 @@ import { inject, injectable, multiInject } from 'inversify'; import { Terminal } from 'vscode'; -import { IConfigurationService } from '../../types'; +import { IConfigurationService, IExperimentService } from '../../types'; import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types'; import { BaseTerminalActivator } from './base'; +import { inTerminalEnvVarExperiment } from '../../experiments/helpers'; +import { shouldEnvExtHandleActivation } from '../../../envExt/api.internal'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry'; @injectable() export class TerminalActivator implements ITerminalActivator { protected baseActivator!: ITerminalActivator; + private pendingActivations = new WeakMap>(); constructor( @inject(ITerminalHelper) readonly helper: ITerminalHelper, @multiInject(ITerminalActivationHandler) private readonly handlers: ITerminalActivationHandler[], - @inject(IConfigurationService) private readonly configurationService: IConfigurationService + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, ) { this.initialize(); } public async activateEnvironmentInTerminal( terminal: Terminal, - options?: TerminalActivationOptions + options?: TerminalActivationOptions, + ): Promise { + let promise = this.pendingActivations.get(terminal); + if (promise) { + return promise; + } + promise = this.activateEnvironmentInTerminalImpl(terminal, options); + this.pendingActivations.set(terminal, promise); + return promise; + } + private async activateEnvironmentInTerminalImpl( + terminal: Terminal, + options?: TerminalActivationOptions, ): Promise { const settings = this.configurationService.getSettings(options?.resource); - const activateEnvironment = settings.terminal.activateEnvironment; - if (!activateEnvironment || options?.hideFromUser) { + const activateEnvironment = + settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService); + if (!activateEnvironment || options?.hideFromUser || shouldEnvExtHandleActivation()) { + if (shouldEnvExtHandleActivation()) { + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL); + } return false; } @@ -33,7 +55,7 @@ export class TerminalActivator implements ITerminalActivator { this.handlers.forEach((handler) => handler .handleActivation(terminal, options?.resource, options?.preserveFocus === true, activated) - .ignoreErrors() + .ignoreErrors(), ); return activated; } diff --git a/src/client/common/terminal/activator/powershellFailedHandler.ts b/src/client/common/terminal/activator/powershellFailedHandler.ts index cdb758ebea54..d580ed4d38bf 100644 --- a/src/client/common/terminal/activator/powershellFailedHandler.ts +++ b/src/client/common/terminal/activator/powershellFailedHandler.ts @@ -7,7 +7,7 @@ import { inject, injectable, named } from 'inversify'; import { Terminal } from 'vscode'; import { PowerShellActivationHackDiagnosticsServiceId, - PowershellActivationNotAvailableDiagnostic + PowershellActivationNotAvailableDiagnostic, } from '../../../application/diagnostics/checks/powerShellActivation'; import { IDiagnosticsService } from '../../../application/diagnostics/types'; import { IPlatformService } from '../../platform/types'; @@ -21,7 +21,7 @@ export class PowershellTerminalActivationFailedHandler implements ITerminalActiv @inject(IPlatformService) private readonly platformService: IPlatformService, @inject(IDiagnosticsService) @named(PowerShellActivationHackDiagnosticsServiceId) - private readonly diagnosticService: IDiagnosticsService + private readonly diagnosticService: IDiagnosticsService, ) {} public async handleActivation(terminal: Terminal, resource: Resource, _preserveFocus: boolean, activated: boolean) { if (activated || !this.platformService.isWindows) { @@ -34,7 +34,7 @@ export class PowershellTerminalActivationFailedHandler implements ITerminalActiv // Check if we can activate in Command Prompt. const activationCommands = await this.helper.getEnvironmentActivationCommands( TerminalShellType.commandPrompt, - resource + resource, ); if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { return; diff --git a/src/client/common/terminal/commandPrompt.ts b/src/client/common/terminal/commandPrompt.ts index 3d83e854e116..4a44557c52a7 100644 --- a/src/client/common/terminal/commandPrompt.ts +++ b/src/client/common/terminal/commandPrompt.ts @@ -19,7 +19,7 @@ export function getCommandPromptLocation(currentProcess: ICurrentProcess) { } export async function useCommandPromptAsDefaultShell( currentProcess: ICurrentProcess, - configService: IConfigurationService + configService: IConfigurationService, ) { const cmdPromptLocation = getCommandPromptLocation(currentProcess); await configService.updateSectionSetting( @@ -27,6 +27,6 @@ export async function useCommandPromptAsDefaultShell( 'integrated.shell.windows', cmdPromptLocation, undefined, - ConfigurationTarget.Global + ConfigurationTarget.Global, ); } diff --git a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts index 215e114f00eb..abc2ff89df63 100644 --- a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts @@ -1,35 +1,75 @@ +/* eslint-disable max-classes-per-file */ // 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 { IInterpreterService } from '../../../interpreter/contracts'; 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'; +type ExecutableFinderFunc = (python: string) => Promise; + +/** + * 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` + */ + +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. + return undefined; + }; +} + @injectable() -export abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider { +abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider { constructor(@inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer) {} public abstract isShellSupported(targetShell: TerminalShellType): boolean; - public getActivationCommands( + + public async getActivationCommands( resource: Uri | undefined, - targetShell: TerminalShellType + targetShell: TerminalShellType, ): Promise { - const pythonPath = this.serviceContainer.get(IConfigurationService).getSettings(resource) - .pythonPath; - return this.getActivationCommandsForInterpreter(pythonPath, targetShell); + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter) { + return undefined; + } + return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); } + public abstract getActivationCommandsForInterpreter( pythonPath: string, - targetShell: TerminalShellType + targetShell: TerminalShellType, ): Promise; } -export type ActivationScripts = Record; +export type ActivationScripts = Partial>; export abstract class VenvBaseActivationCommandProvider extends BaseActivationCommandProvider { public isShellSupported(targetShell: TerminalShellType): boolean { @@ -49,7 +89,7 @@ export abstract class VenvBaseActivationCommandProvider extends BaseActivationCo path.dirname, path.join, // Bind "this"! - (n: string) => fs.fileExists(n) + (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 index b0379ae97c7e..00c4d3da114c 100644 --- a/src/client/common/terminal/environmentActivationProviders/bash.ts +++ b/src/client/common/terminal/environmentActivationProviders/bash.ts @@ -7,7 +7,7 @@ import { TerminalShellType } from '../types'; import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; // For a given shell the scripts are in order of precedence. -const SCRIPTS: ActivationScripts = ({ +const SCRIPTS: ActivationScripts = { // Group 1 [TerminalShellType.wsl]: ['activate.sh', 'activate'], [TerminalShellType.ksh]: ['activate.sh', 'activate'], @@ -18,14 +18,13 @@ const SCRIPTS: ActivationScripts = ({ [TerminalShellType.tcshell]: ['activate.csh'], [TerminalShellType.cshell]: ['activate.csh'], // Group 3 - [TerminalShellType.fish]: ['activate.fish'] -} as unknown) as ActivationScripts; + [TerminalShellType.fish]: ['activate.fish'], +}; 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]) { + for (const names of Object.values(SCRIPTS)) { + for (const name of names) { if (!scripts.includes(name)) { scripts.push(name); } @@ -40,12 +39,12 @@ export class Bash extends VenvBaseActivationCommandProvider { public async getActivationCommandsForInterpreter( pythonPath: string, - targetShell: TerminalShellType + targetShell: TerminalShellType, ): Promise { const scriptFile = await this.findScriptFile(pythonPath, targetShell); if (!scriptFile) { - return; + return undefined; } - return [`source ${scriptFile.fileToCommandArgument()}`]; + return [`source ${scriptFile.fileToCommandArgumentForPythonExt()}`]; } } diff --git a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts index dc8f51ba8aa6..6d40e2c390a0 100644 --- a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts +++ b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts @@ -9,25 +9,24 @@ import { TerminalShellType } from '../types'; import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; // For a given shell the scripts are in order of precedence. -const SCRIPTS: ActivationScripts = ({ +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; + [TerminalShellType.powershellCore]: ['Activate.ps1', 'activate.bat'], +}; 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]) { + for (const names of Object.values(SCRIPTS)) { + for (const name of names) { if (!scripts.includes(name)) { scripts.push( name, // We also add scripts in subdirs. pathJoin('Scripts', name), - pathJoin('scripts', name) + pathJoin('scripts', name), ); } } @@ -38,18 +37,19 @@ export function getAllScripts(pathJoin: (...p: string[]) => string): string[] { @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)) { + this.scripts = {}; + for (const [key, names] of Object.entries(SCRIPTS)) { const shell = key as TerminalShellType; const scripts: string[] = []; - for (const name of SCRIPTS[shell]) { + for (const name of names) { scripts.push( name, // We also add scripts in subdirs. path.join('Scripts', name), - path.join('scripts', name) + path.join('scripts', name), ); } this.scripts[shell] = scripts; @@ -58,25 +58,27 @@ export class CommandPromptAndPowerShell extends VenvBaseActivationCommandProvide public async getActivationCommandsForInterpreter( pythonPath: string, - targetShell: TerminalShellType + targetShell: TerminalShellType, ): Promise { const scriptFile = await this.findScriptFile(pythonPath, targetShell); if (!scriptFile) { - return; + return undefined; } if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.bat')) { - return [scriptFile.fileToCommandArgument()]; - } else if ( + return [scriptFile.fileToCommandArgumentForPythonExt()]; + } + if ( (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) && scriptFile.endsWith('Activate.ps1') ) { - return [`& ${scriptFile.fileToCommandArgument()}`]; - } else if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('Activate.ps1')) { + return [`& ${scriptFile.fileToCommandArgumentForPythonExt()}`]; + } + 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; } + + return undefined; } } diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts index 3f4b5cfaa809..42bb8f38fc9e 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -1,22 +1,20 @@ // 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 { traceInfo, traceVerbose, traceWarn } from '../../../logging'; -import { ICondaService } from '../../../interpreter/contracts'; +import { IComponentAdapter, 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). */ @@ -25,13 +23,15 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman constructor( @inject(ICondaService) private readonly condaService: ICondaService, @inject(IPlatformService) private platform: IPlatformService, - @inject(IConfigurationService) private configService: IConfigurationService + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IComponentAdapter) private pyenvs: IComponentAdapter, ) {} /** * Is the given shell supported for activating a conda env? */ - public isShellSupported(_targetShell: TerminalShellType): boolean { + // eslint-disable-next-line class-methods-use-this + public isShellSupported(): boolean { return true; } @@ -40,9 +40,9 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman */ public getActivationCommands( resource: Uri | undefined, - targetShell: TerminalShellType + targetShell: TerminalShellType, ): Promise { - const pythonPath = this.configService.getSettings(resource).pythonPath; + const { pythonPath } = this.configService.getSettings(resource); return this.getActivationCommandsForInterpreter(pythonPath, targetShell); } @@ -52,60 +52,85 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman */ public async getActivationCommandsForInterpreter( pythonPath: string, - targetShell: TerminalShellType + targetShell: TerminalShellType, ): Promise { - const envInfo = await this.condaService.getCondaEnvironment(pythonPath); + traceVerbose(`Getting conda activation commands for interpreter ${pythonPath} with shell ${targetShell}`); + const envInfo = await this.pyenvs.getCondaEnvironment(pythonPath); if (!envInfo) { - return; + traceWarn(`No conda environment found for interpreter ${pythonPath}`); + return undefined; } + traceVerbose(`Found conda environment: ${JSON.stringify(envInfo)}`); 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. + // New version. + const interpreterPath = await this.condaService.getInterpreterPathForEnvironment(envInfo); + traceInfo(`Using interpreter path: ${interpreterPath}`); + const activatePath = await this.condaService.getActivationScriptFromInterpreter(interpreterPath, envInfo.name); + traceVerbose(`Got activation script: ${activatePath?.path}} with type: ${activatePath?.type}`); + // eslint-disable-next-line camelcase + if (activatePath?.path) { if ( - versionInfo.minor >= CondaRequiredMinorForPowerShell && - (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) + this.platform.isWindows && + targetShell !== TerminalShellType.bash && + targetShell !== TerminalShellType.gitbash ) { - return this.getPowershellCommands(condaEnv); + const commands = [activatePath.path, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using Windows-specific commands: ${commands.join(', ')}`); + return commands; } - 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()}`]; + + const condaInfo = await this.condaService.getCondaInfo(); + + traceVerbose(`Conda shell level: ${condaInfo?.conda_shlvl}`); + if ( + activatePath.type !== 'global' || + // eslint-disable-next-line camelcase + condaInfo?.conda_shlvl === undefined || + condaInfo.conda_shlvl === -1 + ) { + // activatePath is not the global activate path, or we don't have a shlvl, or it's -1(conda never sourced). + // and we need to source the activate path. + if (activatePath.path === 'activate') { + const commands = [ + `source ${activatePath.path}`, + `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`, + ]; + traceInfo(`Using source activate commands: ${commands.join(', ')}`); + return commands; } + const command = [`source ${activatePath.path} ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using single source command: ${command}`); + return command; } + const command = [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using direct conda activate command: ${command}`); + return command; } switch (targetShell) { case TerminalShellType.powershell: case TerminalShellType.powershellCore: - return this.getPowershellCommands(condaEnv); + traceVerbose('Using PowerShell-specific activation'); + return _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()); + traceVerbose('Using Fish shell-specific activation'); + return getFishCommands(condaEnv, await this.condaService.getCondaFile()); default: if (this.platform.isWindows) { + traceVerbose('Using Windows shell-specific activation fallback option.'); return this.getWindowsCommands(condaEnv); - } else { - return this.getUnixCommands(condaEnv, await this.condaService.getCondaFile()); } + return getUnixCommands(condaEnv, await this.condaService.getCondaFile()); } } public async getWindowsActivateCommand(): Promise { - let activateCmd: string = 'activate'; + let activateCmd = 'activate'; const condaExePath = await this.condaService.getCondaFile(); @@ -113,7 +138,7 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman 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(); + activateCmd = activateCmd.toCommandArgumentForPythonExt(); } return activateCmd; @@ -121,30 +146,27 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman 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()}`]; + return [`${activate} ${condaEnv.toCommandArgumentForPythonExt()}`]; } +} - 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()}`]; - } +/** + * 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. + */ +export async function _getPowershellCommands(condaEnv: string): Promise { + return [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; +} - 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()}`]; - } +async function getFishCommands(condaEnv: string, condaFile: string): Promise { + // https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28 + return [`${condaFile.fileToCommandArgumentForPythonExt()} activate ${condaEnv.toCommandArgumentForPythonExt()}`]; +} + +async function getUnixCommands(condaEnv: string, condaFile: string): Promise { + const condaDir = path.dirname(condaFile); + const activateFile = path.join(condaDir, 'activate'); + return [`source ${activateFile.fileToCommandArgumentForPythonExt()} ${condaEnv.toCommandArgumentForPythonExt()}`]; } diff --git a/src/client/common/terminal/environmentActivationProviders/nushell.ts b/src/client/common/terminal/environmentActivationProviders/nushell.ts new file mode 100644 index 000000000000..333fd5167770 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/nushell.ts @@ -0,0 +1,40 @@ +// 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 = { + [TerminalShellType.nushell]: ['activate.nu'], +}; + +export function getAllScripts(): string[] { + const scripts: string[] = []; + for (const names of Object.values(SCRIPTS)) { + for (const name of names) { + if (!scripts.includes(name)) { + scripts.push(name); + } + } + } + return scripts; +} + +@injectable() +export class Nushell extends VenvBaseActivationCommandProvider { + protected readonly scripts = SCRIPTS; + + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise { + const scriptFile = await this.findScriptFile(pythonPath, targetShell); + if (!scriptFile) { + return undefined; + } + return [`overlay use ${scriptFile.fileToCommandArgumentForPythonExt()}`]; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts index f6ecf8c1b24a..d097c759ec40 100644 --- a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts @@ -3,56 +3,54 @@ 'use strict'; -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; import { Uri } from 'vscode'; -import '../../../common/extensions'; -import { IInterpreterService, IPipEnvService } from '../../../interpreter/contracts'; -import { InterpreterType } from '../../../pythonEnvironments/info'; +import '../../extensions'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { isPipenvEnvironmentRelatedToFolder } from '../../../pythonEnvironments/common/environmentManagers/pipenv'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; import { IWorkspaceService } from '../../application/types'; -import { IFileSystem } from '../../platform/types'; -import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; +import { IToolExecutionPath, ToolExecutionPath } from '../../types'; +import { ITerminalActivationCommandProvider } from '../types'; @injectable() export class PipEnvActivationCommandProvider implements ITerminalActivationCommandProvider { constructor( @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IPipEnvService) private readonly pipenvService: IPipEnvService, + @inject(IToolExecutionPath) + @named(ToolExecutionPath.pipenv) + private readonly pipEnvExecution: IToolExecutionPath, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IFileSystem) private readonly fs: IFileSystem ) {} - public isShellSupported(_targetShell: TerminalShellType): boolean { + // eslint-disable-next-line class-methods-use-this + public isShellSupported(): boolean { return false; } - public async getActivationCommands(resource: Uri | undefined, _: TerminalShellType): Promise { + public async getActivationCommands(resource: Uri | undefined): Promise { const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (!interpreter || interpreter.type !== InterpreterType.Pipenv) { - return; + if (!interpreter || interpreter.envType !== EnvironmentType.Pipenv) { + return undefined; } // 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; + if (workspaceFolder) { + if (!(await isPipenvEnvironmentRelatedToFolder(interpreter.path, workspaceFolder?.uri.fsPath))) { + return undefined; + } } - const execName = this.pipenvService.executable; - return [`${execName.fileToCommandArgument()} shell`]; + const execName = this.pipEnvExecution.executable; + return [`${execName.fileToCommandArgumentForPythonExt()} shell`]; } - public async getActivationCommandsForInterpreter( - pythonPath: string, - _targetShell: TerminalShellType - ): Promise { + public async getActivationCommandsForInterpreter(pythonPath: string): Promise { const interpreter = await this.interpreterService.getInterpreterDetails(pythonPath); - if (!interpreter || interpreter.type !== InterpreterType.Pipenv) { - return; + if (!interpreter || interpreter.envType !== EnvironmentType.Pipenv) { + return undefined; } - const execName = this.pipenvService.executable; - return [`${execName.fileToCommandArgument()} shell`]; + const execName = this.pipEnvExecution.executable; + return [`${execName.fileToCommandArgumentForPythonExt()} shell`]; } } diff --git a/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts new file mode 100644 index 000000000000..1deaa56dd8ae --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts @@ -0,0 +1,77 @@ +/* eslint-disable class-methods-use-this */ +// 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 { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; +import { getPixiActivationCommands } from '../../../pythonEnvironments/common/environmentManagers/pixi'; + +@injectable() +export class PixiActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) {} + + // eslint-disable-next-line class-methods-use-this + public isShellSupported(targetShell: TerminalShellType): boolean { + return shellTypeToPixiShell(targetShell) !== undefined; + } + + public async getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType, + ): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + return undefined; + } + + return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); + } + + public getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise { + return getPixiActivationCommands(pythonPath, targetShell); + } +} + +/** + * Returns the name of a terminal shell type within Pixi. + */ +function shellTypeToPixiShell(targetShell: TerminalShellType): string | undefined { + switch (targetShell) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.commandPrompt: + return 'cmd'; + + case TerminalShellType.zsh: + return 'zsh'; + + case TerminalShellType.fish: + return 'fish'; + + case TerminalShellType.nushell: + return 'nushell'; + + case TerminalShellType.xonsh: + return 'xonsh'; + + case TerminalShellType.cshell: + // Explicitly unsupported + return undefined; + + case TerminalShellType.gitbash: + case TerminalShellType.bash: + case TerminalShellType.wsl: + case TerminalShellType.tcshell: + case TerminalShellType.other: + default: + return 'bash'; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts index f5b1ba3a6f4f..6b5ced048672 100644 --- a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts @@ -7,13 +7,14 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IInterpreterService } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; -import { InterpreterType } from '../../../pythonEnvironments/info'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; @injectable() export class PyEnvActivationCommandProvider implements ITerminalActivationCommandProvider { constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + // eslint-disable-next-line class-methods-use-this public isShellSupported(_targetShell: TerminalShellType): boolean { return true; } @@ -22,24 +23,24 @@ export class PyEnvActivationCommandProvider implements ITerminalActivationComman const interpreter = await this.serviceContainer .get(IInterpreterService) .getActiveInterpreter(resource); - if (!interpreter || interpreter.type !== InterpreterType.Pyenv || !interpreter.envName) { - return; + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return undefined; } - return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; } public async getActivationCommandsForInterpreter( pythonPath: string, - _targetShell: TerminalShellType + _targetShell: TerminalShellType, ): Promise { const interpreter = await this.serviceContainer .get(IInterpreterService) .getInterpreterDetails(pythonPath); - if (!interpreter || interpreter.type !== InterpreterType.Pyenv || !interpreter.envName) { - return; + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return undefined; } - return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; } } diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index ba47a386765a..39cc88c4b024 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -3,12 +3,12 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; +import * as path from 'path'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +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'; @@ -20,29 +20,21 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IFileSystem) private fs: IFileSystem, - @inject(IInterpreterService) private interpreterService: IInterpreterService + @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); + public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { + const resource = options?.resource; + const title = options?.title; + let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + const interpreter = options?.interpreter; + const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); if (!this.terminalServices.has(id)) { + if (resource && options.newTerminalPerFile) { + terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; + } + options.title = terminalTitle; const terminalService = new TerminalService(this.serviceContainer, options); this.terminalServices.set(id, terminalService); } @@ -52,20 +44,26 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { this.fs, this.interpreterService, this.terminalServices.get(id)!, - interpreter + 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?: PythonInterpreter): string { + private getTerminalId( + title: string, + resource?: Uri, + interpreter?: PythonEnvironment, + newTerminalPerFile?: boolean, + ): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}`; + const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; } } diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index ab6bd6cb1fb1..d2b3bb7879af 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -3,13 +3,14 @@ import { inject, injectable, multiInject, named } from 'inversify'; import { Terminal, Uri } from 'vscode'; -import { ICondaService, IInterpreterService } from '../../interpreter/contracts'; -import { InterpreterType, PythonInterpreter } from '../../pythonEnvironments/info'; +import { IComponentAdapter, IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { traceDecoratorError, traceError } from '../../logging'; +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'; @@ -19,8 +20,9 @@ import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalActivationProviders, - TerminalShellType + TerminalShellType, } from './types'; +import { isPixiEnvironment } from '../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class TerminalHelper implements ITerminalHelper { @@ -28,7 +30,7 @@ export class TerminalHelper implements ITerminalHelper { constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, - @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @inject(IInterpreterService) readonly interpreterService: IInterpreterService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @inject(ITerminalActivationCommandProvider) @@ -41,12 +43,18 @@ export class TerminalHelper implements ITerminalHelper { @named(TerminalActivationProviders.commandPromptAndPowerShell) private readonly commandPromptAndPowerShell: ITerminalActivationCommandProvider, @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.nushell) + private readonly nushell: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pyenv) private readonly pyenv: ITerminalActivationCommandProvider, @inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pipenv) private readonly pipenv: ITerminalActivationCommandProvider, - @multiInject(IShellDetector) shellDetectors: IShellDetector[] + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.pixi) + private readonly pixi: ITerminalActivationCommandProvider, + @multiInject(IShellDetector) shellDetectors: IShellDetector[], ) { this.shellDetector = new ShellDetector(this.platform, shellDetectors); } @@ -62,49 +70,56 @@ export class TerminalHelper implements ITerminalHelper { terminalShellType === TerminalShellType.powershell || terminalShellType === TerminalShellType.powershellCore; const commandPrefix = isPowershell ? '& ' : ''; - const formattedArgs = args.map((a) => a.toCommandArgument()); + const formattedArgs = args.map((a) => a.toCommandArgumentForPythonExt()); - return `${commandPrefix}${command.fileToCommandArgument()} ${formattedArgs.join(' ')}`.trim(); + return `${commandPrefix}${command.fileToCommandArgumentForPythonExt()} ${formattedArgs.join(' ')}`.trim(); } public async getEnvironmentActivationCommands( terminalShellType: TerminalShellType, resource?: Uri, - interpreter?: PythonInterpreter + interpreter?: PythonEnvironment, ): Promise { - const providers = [this.pipenv, this.pyenv, this.bashCShellFish, this.commandPromptAndPowerShell]; + const providers = [ + this.pixi, + this.pipenv, + this.pyenv, + this.bashCShellFish, + this.commandPromptAndPowerShell, + this.nushell, + ]; const promise = this.getActivationCommands(resource || undefined, interpreter, terminalShellType, providers); this.sendTelemetry( terminalShellType, EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL, interpreter, - promise + promise, ).ignoreErrors(); return promise; } public async getEnvironmentActivationShellCommands( resource: Resource, shell: TerminalShellType, - interpreter?: PythonInterpreter + interpreter?: PythonEnvironment, ): Promise { if (this.platform.osType === OSType.Unknown) { return; } - const providers = [this.bashCShellFish, this.commandPromptAndPowerShell]; + const providers = [this.pixi, this.bashCShellFish, this.commandPromptAndPowerShell, this.nushell]; const promise = this.getActivationCommands(resource, interpreter, shell, providers); this.sendTelemetry( shell, EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE, interpreter, - promise + promise, ).ignoreErrors(); return promise; } - @traceDecorators.error('Failed to capture telemetry') + @traceDecoratorError('Failed to capture telemetry') protected async sendTelemetry( terminalShellType: TerminalShellType, eventName: EventName, - interpreter: PythonInterpreter | undefined, - promise: Promise + interpreter: PythonEnvironment | undefined, + promise: Promise, ): Promise { let hasCommands = false; let failed = false; @@ -117,22 +132,36 @@ export class TerminalHelper implements ITerminalHelper { } const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; - const interpreterType = interpreter ? interpreter.type : InterpreterType.Unknown; + 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: PythonInterpreter | undefined, + interpreter: PythonEnvironment | undefined, terminalShellType: TerminalShellType, - providers: ITerminalActivationCommandProvider[] + providers: ITerminalActivationCommandProvider[], ): Promise { const settings = this.configurationService.getSettings(resource); + const isPixiEnv = interpreter + ? interpreter.envType === EnvironmentType.Pixi + : await isPixiEnvironment(settings.pythonPath); + if (isPixiEnv) { + const activationCommands = interpreter + ? await this.pixi.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await this.pixi.getActivationCommands(resource, terminalShellType); + + if (Array.isArray(activationCommands)) { + return activationCommands; + } + } + + const condaService = this.serviceContainer.get(IComponentAdapter); // If we have a conda environment, then use that. const isCondaEnvironment = interpreter - ? interpreter.type === InterpreterType.Conda - : await this.condaService.isCondaEnvironment(settings.pythonPath); + ? interpreter.envType === EnvironmentType.Conda + : await condaService.isCondaEnvironment(settings.pythonPath); if (isCondaEnvironment) { const activationCommands = interpreter ? await this.conda.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index f819f654f5d7..0dffd5615ae1 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -2,21 +2,27 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { CancellationToken, Disposable, Event, EventEmitter, Terminal } from 'vscode'; +import { CancellationToken, Disposable, Event, EventEmitter, Terminal, TerminalShellExecution } 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 { ITerminalAutoActivation } from '../../terminals/types'; +import { IApplicationShell, ITerminalManager } from '../application/types'; +import { _SCRIPTS_DIR } from '../process/internal/scripts/constants'; import { IConfigurationService, IDisposableRegistry } from '../types'; import { ITerminalActivator, ITerminalHelper, ITerminalService, TerminalCreationOptions, - TerminalShellType + TerminalShellType, } from './types'; +import { traceVerbose } from '../../logging'; +import { sleep } from '../utils/async'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { ensureTerminalLegacy } from '../../envExt/api.legacy'; @injectable() export class TerminalService implements ITerminalService, Disposable { @@ -26,23 +32,39 @@ export class TerminalService implements ITerminalService, Disposable { private terminalManager: ITerminalManager; private terminalHelper: ITerminalHelper; private terminalActivator: ITerminalActivator; + private terminalAutoActivator: ITerminalAutoActivation; + private applicationShell: IApplicationShell; + private readonly executeCommandListeners: Set = new Set(); + private _terminalFirstLaunched: boolean = true; + private pythonReplCommandQueue: string[] = []; + private isReplReady: boolean = false; + private replPromptListener?: Disposable; + private replShellTypeListener?: Disposable; public get onDidCloseTerminal(): Event { return this.terminalClosed.event.bind(this.terminalClosed); } + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, - private readonly options?: TerminalCreationOptions + 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.terminalAutoActivator = this.serviceContainer.get(ITerminalAutoActivation); + this.applicationShell = this.serviceContainer.get(IApplicationShell); this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); this.terminalActivator = this.serviceContainer.get(ITerminalActivator); } public dispose() { - if (this.terminal) { - this.terminal.dispose(); + this.terminal?.dispose(); + this.disposeReplListener(); + + if (this.executeCommandListeners && this.executeCommandListeners.size > 0) { + this.executeCommandListeners.forEach((d) => { + d?.dispose(); + }); } } public async sendCommand(command: string, args: string[], _?: CancellationToken): Promise { @@ -51,8 +73,10 @@ export class TerminalService implements ITerminalService, Disposable { if (!this.options?.hideFromUser) { this.terminal!.show(true); } - this.terminal!.sendText(text, true); + + await this.executeCommand(text, false); } + /** @deprecated */ public async sendText(text: string): Promise { await this.ensureTerminal(); if (!this.options?.hideFromUser) { @@ -60,43 +84,175 @@ export class TerminalService implements ITerminalService, Disposable { } this.terminal!.sendText(text); } + public async executeCommand( + commandLine: string, + isPythonShell: boolean, + ): Promise { + if (isPythonShell) { + if (this.isReplReady) { + this.terminal?.sendText(commandLine); + traceVerbose(`Python REPL sendText: ${commandLine}`); + } else { + // Queue command to run once REPL is ready. + this.pythonReplCommandQueue.push(commandLine); + traceVerbose(`Python REPL queued command: ${commandLine}`); + this.startReplListener(); + } + return undefined; + } + + // Non-REPL code execution + return this.executeCommandInternal(commandLine); + } + + private startReplListener(): void { + if (this.replPromptListener || this.replShellTypeListener) { + return; + } + + this.replShellTypeListener = this.terminalManager.onDidChangeTerminalState((terminal) => { + if (this.terminal && terminal === this.terminal) { + if (terminal.state.shell == 'python') { + traceVerbose('Python REPL ready from terminal shell api'); + this.onReplReady(); + } + } + }); + + let terminalData = ''; + this.replPromptListener = this.applicationShell.onDidWriteTerminalData((e) => { + if (this.terminal && e.terminal === this.terminal) { + terminalData += e.data; + if (/>>>\s*$/.test(terminalData)) { + traceVerbose('Python REPL ready, from >>> prompt detection'); + this.onReplReady(); + } + } + }); + } + + private onReplReady(): void { + if (this.isReplReady) { + return; + } + this.isReplReady = true; + this.flushReplQueue(); + this.disposeReplListener(); + } + + private disposeReplListener(): void { + if (this.replPromptListener) { + this.replPromptListener.dispose(); + this.replPromptListener = undefined; + } + if (this.replShellTypeListener) { + this.replShellTypeListener.dispose(); + this.replShellTypeListener = undefined; + } + } + + private flushReplQueue(): void { + while (this.pythonReplCommandQueue.length > 0) { + const commandLine = this.pythonReplCommandQueue.shift(); + if (commandLine) { + traceVerbose(`Executing queued REPL command: ${commandLine}`); + this.terminal?.sendText(commandLine); + } + } + } + + private async executeCommandInternal(commandLine: string): Promise { + const terminal = this.terminal; + if (!terminal) { + traceVerbose('Terminal not available, cannot execute command'); + return undefined; + } + + if (!this.options?.hideFromUser) { + terminal.show(true); + } + + // If terminal was just launched, wait some time for shell integration to onDidChangeShellIntegration. + if (!terminal.shellIntegration && this._terminalFirstLaunched) { + this._terminalFirstLaunched = false; + const promise = new Promise((resolve) => { + const disposable = this.terminalManager.onDidChangeTerminalShellIntegration(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + clearTimeout(timer); + disposable.dispose(); + resolve(true); + }); + const TIMEOUT_DURATION = 500; + const timer = setTimeout(() => { + disposable.dispose(); + resolve(true); + }, TIMEOUT_DURATION); + }); + await promise; + } + + if (terminal.shellIntegration) { + const execution = terminal.shellIntegration.executeCommand(commandLine); + traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`); + return execution; + } else { + terminal.sendText(commandLine); + traceVerbose(`Shell Integration is disabled, sendText: ${commandLine}`); + } + + return undefined; + } + 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 { + // TODO: Debt switch to Promise ---> breaks 20 tests + public 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)); + if (useEnvExtension()) { + this.terminal = await ensureTerminalLegacy(this.options?.resource, { + name: this.options?.title || 'Python', + hideFromUser: this.options?.hideFromUser, + }); + return; + } else { + this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); + this.terminal = this.terminalManager.createTerminal({ + name: this.options?.title || 'Python', + hideFromUser: this.options?.hideFromUser, + }); + this.terminalAutoActivator.disableAutoActivation(this.terminal); - await this.terminalActivator.activateEnvironmentInTerminal(this.terminal!, { - resource: this.options?.resource, - preserveFocus, - interpreter: this.options?.interpreter, - hideFromUser: this.options?.hideFromUser - }); + await sleep(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.terminal.show(preserveFocus); } this.sendTelemetry().ignoreErrors(); + return; } private terminalCloseHandler(terminal: Terminal) { if (terminal === this.terminal) { this.terminalClosed.fire(); this.terminal = undefined; + this.isReplReady = false; + this.disposeReplListener(); + this.pythonReplCommandQueue = []; } } @@ -110,11 +266,11 @@ export class TerminalService implements ITerminalService, Disposable { .get(IInterpreterService) .getInterpreterDetails(pythonPath)); const pythonVersion = interpreterInfo && interpreterInfo.version ? interpreterInfo.version.raw : undefined; - const interpreterType = interpreterInfo ? interpreterInfo.type : undefined; + const interpreterType = interpreterInfo ? interpreterInfo.envType : undefined; captureTelemetry(EventName.TERMINAL_CREATE, { terminal: this.terminalShellType, pythonVersion, - interpreterType + interpreterType, }); } } diff --git a/src/client/common/terminal/shellDetector.ts b/src/client/common/terminal/shellDetector.ts index 9a911a26bc0b..bf183f20a279 100644 --- a/src/client/common/terminal/shellDetector.ts +++ b/src/client/common/terminal/shellDetector.ts @@ -4,11 +4,11 @@ 'use strict'; import { inject, injectable, multiInject } from 'inversify'; -import { Terminal } from 'vscode'; +import { Terminal, env } from 'vscode'; +import { traceError, traceVerbose } from '../../logging'; 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'; @@ -17,26 +17,22 @@ const defaultOSShells = { [OSType.Linux]: TerminalShellType.bash, [OSType.OSX]: TerminalShellType.bash, [OSType.Windows]: TerminalShellType.commandPrompt, - [OSType.Unknown]: TerminalShellType.other + [OSType.Unknown]: TerminalShellType.other, }; @injectable() export class ShellDetector { constructor( @inject(IPlatformService) private readonly platform: IPlatformService, - @multiInject(IShellDetector) private readonly shellDetectors: IShellDetector[] + @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. + * 2. Try to identify the type of the shell based on the settings 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 + * More information here: https://github.com/microsoft/vscode/issues/74233#issuecomment-497527337 */ public identifyTerminalShell(terminal?: Terminal): TerminalShellType { let shell: TerminalShellType | undefined; @@ -45,7 +41,7 @@ export class ShellDetector { shellIdentificationSource: 'default', terminalProvided: !!terminal, hasCustomShell: undefined, - hasShellInEnv: undefined + hasShellInEnv: undefined, }; // Sort in order of priority and then identify the shell. @@ -53,10 +49,8 @@ export class ShellDetector { 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) { + telemetryProperties.failed = false; break; } } @@ -65,10 +59,11 @@ export class ShellDetector { // 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}'`); + traceVerbose(`Shell identified as ${shell} ${terminal ? `(Terminal name is ${terminal.name})` : ''}`); // If we could not identify the shell, use the defaults. if (shell === undefined || shell === TerminalShellType.other) { + traceError('Unable to identify shell', env.shell, ' for OS ', this.platform.osType); traceVerbose('Using default OS shell'); shell = defaultOSShells[this.platform.osType]; } diff --git a/src/client/common/terminal/shellDetectors/baseShellDetector.ts b/src/client/common/terminal/shellDetectors/baseShellDetector.ts index 04c534dedf06..4262bdf80364 100644 --- a/src/client/common/terminal/shellDetectors/baseShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/baseShellDetector.ts @@ -5,11 +5,8 @@ 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). @@ -21,17 +18,18 @@ When identifying the shell use the following algorithm: // 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_GITBASH = /(gitbash$)/i; +const IS_BASH = /(bash$)/i; +const IS_WSL = /(wsl$)/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_COMMAND = /(cmd$)/i; +const IS_POWERSHELL = /(powershell$)/i; +const IS_POWERSHELL_CORE = /(pwsh$)/i; const IS_FISH = /(fish$)/i; const IS_CSHELL = /(csh$)/i; const IS_TCSHELL = /(tcsh$)/i; +const IS_NUSHELL = /(nu$)/i; const IS_XONSH = /(xonsh$)/i; const detectableShells = new Map(); @@ -45,6 +43,7 @@ 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.nushell, IS_NUSHELL); detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); detectableShells.set(TerminalShellType.xonsh, IS_XONSH); @@ -53,21 +52,27 @@ export abstract class BaseShellDetector implements IShellDetector { constructor(@unmanaged() public readonly priority: number) {} public abstract identify( telemetryProperties: ShellIdentificationTelemetry, - terminal?: Terminal + 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 identifyShellFromShellPath(shellPath); + } +} + +export function identifyShellFromShellPath(shellPath: string): TerminalShellType { + // Remove .exe extension so shells can be more consistently detected + // on Windows (including Cygwin). + const basePath = shellPath.replace(/\.exe$/i, ''); + + const shell = Array.from(detectableShells.keys()).reduce((matchedShell, shellToDetect) => { + if (matchedShell === TerminalShellType.other) { + const pat = detectableShells.get(shellToDetect); + if (pat && pat.test(basePath)) { + return shellToDetect; } - return matchedShell; - }, TerminalShellType.other); + } + return matchedShell; + }, TerminalShellType.other); - traceVerbose(`Shell path '${shellPath}'`); - traceVerbose(`Shell path identified as shell '${shell}'`); - return shell; - } + return shell; } diff --git a/src/client/common/terminal/shellDetectors/settingsShellDetector.ts b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts index 5fb315cc1c2c..6288675ec3f8 100644 --- a/src/client/common/terminal/shellDetectors/settingsShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts @@ -6,7 +6,6 @@ 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'; @@ -14,16 +13,12 @@ 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 + @inject(IPlatformService) private readonly platform: IPlatformService, ) { super(2); } @@ -51,7 +46,7 @@ export class SettingsShellDetector extends BaseShellDetector { } public identify( telemetryProperties: ShellIdentificationTelemetry, - _terminal?: Terminal + _terminal?: Terminal, ): TerminalShellType | undefined { const shellPath = this.getTerminalShellPath(); telemetryProperties.hasCustomShell = !!shellPath; @@ -59,9 +54,9 @@ export class SettingsShellDetector extends BaseShellDetector { if (shell !== TerminalShellType.other) { telemetryProperties.shellIdentificationSource = 'environment'; + } else { + telemetryProperties.shellIdentificationSource = 'settings'; } - 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 index 34ca634d3936..0f14adbe9d36 100644 --- a/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts @@ -5,16 +5,12 @@ import { injectable } from 'inversify'; import { Terminal } from 'vscode'; -import { traceVerbose } from '../../logger'; +import { traceVerbose } from '../../../logging'; 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 { @@ -23,7 +19,7 @@ export class TerminalNameShellDetector extends BaseShellDetector { } public identify( telemetryProperties: ShellIdentificationTelemetry, - terminal?: Terminal + terminal?: Terminal, ): TerminalShellType | undefined { if (!terminal) { return; diff --git a/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts index 3f655da212ca..da84eef4d46f 100644 --- a/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts @@ -5,7 +5,6 @@ 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'; @@ -14,16 +13,12 @@ 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 + @inject(IPlatformService) private readonly platform: IPlatformService, ) { super(1); } @@ -32,7 +27,7 @@ export class UserEnvironmentShellDetector extends BaseShellDetector { } public identify( telemetryProperties: ShellIdentificationTelemetry, - _terminal?: Terminal + _terminal?: Terminal, ): TerminalShellType | undefined { const shellPath = this.getDefaultPlatformShell(); telemetryProperties.hasShellInEnv = !!shellPath; @@ -41,7 +36,6 @@ export class UserEnvironmentShellDetector extends BaseShellDetector { if (shell !== TerminalShellType.other) { telemetryProperties.shellIdentificationSource = 'environment'; } - traceVerbose(`Shell path from user env '${shellPath}'`); return shell; } } diff --git a/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts index e39197fbea7b..9ca1b8c4ec22 100644 --- a/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts @@ -5,17 +5,13 @@ import { inject } from 'inversify'; import { Terminal } from 'vscode'; +import { traceVerbose } from '../../../logging'; 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) { @@ -23,15 +19,20 @@ export class VSCEnvironmentShellDetector extends BaseShellDetector { } public identify( telemetryProperties: ShellIdentificationTelemetry, - _terminal?: Terminal + terminal?: Terminal, ): TerminalShellType | undefined { - if (!this.appEnv.shell) { + const shellPath = + terminal?.creationOptions && 'shellPath' in terminal.creationOptions && terminal.creationOptions.shellPath + ? terminal.creationOptions.shellPath + : this.appEnv.shell; + if (!shellPath) { return; } - const shell = this.identifyShellFromShellPath(this.appEnv.shell); - traceVerbose(`Terminal shell path '${this.appEnv.shell}' identified as shell '${shell}'`); + const shell = this.identifyShellFromShellPath(shellPath); + traceVerbose(`Terminal shell path '${shellPath}' identified as shell '${shell}'`); telemetryProperties.shellIdentificationSource = shell === TerminalShellType.other ? telemetryProperties.shellIdentificationSource : 'vscode'; + telemetryProperties.failed = shell === TerminalShellType.other ? false : true; return shell; } } diff --git a/src/client/common/terminal/syncTerminalService.ts b/src/client/common/terminal/syncTerminalService.ts index 546ce3a4a223..0b46a86ee51e 100644 --- a/src/client/common/terminal/syncTerminalService.ts +++ b/src/client/common/terminal/syncTerminalService.ts @@ -4,11 +4,11 @@ 'use strict'; import { inject } from 'inversify'; -import { CancellationToken, Disposable, Event } from 'vscode'; +import { CancellationToken, Disposable, Event, TerminalShellExecution } from 'vscode'; import { IInterpreterService } from '../../interpreter/contracts'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { traceVerbose } from '../../logging'; +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'; @@ -20,7 +20,7 @@ enum State { notStarted = 0, started = 1, completed = 2, - errored = 4 + errored = 4, } class ExecutionState implements Disposable { @@ -30,7 +30,7 @@ class ExecutionState implements Disposable { constructor( public readonly lockFile: string, private readonly fs: IFileSystem, - private readonly command: string[] + private readonly command: string[], ) { this.registerStateUpdate(); this._completed.promise.finally(() => this.dispose()).ignoreErrors(); @@ -56,9 +56,9 @@ class ExecutionState implements Disposable { this._completed.reject( new Error( `Command failed with errors, check the terminal for details. Command: ${this.command.join( - ' ' - )}\n${errorContents}` - ) + ' ', + )}\n${errorContents}`, + ), ); } else if (state & State.completed) { this._completed.resolve(); @@ -66,8 +66,7 @@ class ExecutionState implements Disposable { }, 100); this.disposable = { - // tslint:disable-next-line: no-any - dispose: () => clearInterval(timeout as any) + dispose: () => clearInterval(timeout as any), }; } private async getLockFileState(file: string): Promise { @@ -93,11 +92,6 @@ class ExecutionState implements Disposable { * - 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[] = []; @@ -108,7 +102,7 @@ export class SynchronousTerminalService implements ITerminalService, Disposable @inject(IFileSystem) private readonly fs: IFileSystem, @inject(IInterpreterService) private readonly interpreter: IInterpreterService, public readonly terminalService: TerminalService, - private readonly pythonInterpreter?: PythonInterpreter + private readonly pythonInterpreter?: PythonEnvironment, ) {} public dispose() { this.terminalService.dispose(); @@ -129,7 +123,7 @@ export class SynchronousTerminalService implements ITerminalService, Disposable command: string, args: string[], cancel?: CancellationToken, - swallowExceptions: boolean = true + swallowExceptions: boolean = true, ): Promise { if (!cancel) { return this.terminalService.sendCommand(command, args); @@ -147,9 +141,13 @@ export class SynchronousTerminalService implements ITerminalService, Disposable lockFile.dispose(); } } + /** @deprecated */ public sendText(text: string): Promise { return this.terminalService.sendText(text); } + public executeCommand(commandLine: string, isPythonShell: boolean): Promise { + return this.terminalService.executeCommand(commandLine, isPythonShell); + } public show(preserveFocus?: boolean | undefined): Promise { return this.terminalService.show(preserveFocus); } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index db5a0fe500da..3e54458a57fd 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -3,17 +3,19 @@ 'use strict'; -import { CancellationToken, Event, Terminal, Uri } from 'vscode'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { CancellationToken, Event, Terminal, Uri, TerminalShellExecution } from 'vscode'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; import { IEventNamePropertyMapping } from '../../telemetry/index'; import { IDisposable, Resource } from '../types'; export enum TerminalActivationProviders { bashCShellFish = 'bashCShellFish', commandPromptAndPowerShell = 'commandPromptAndPowerShell', + nushell = 'nushell', pyenv = 'pyenv', conda = 'conda', - pipenv = 'pipenv' + pipenv = 'pipenv', + pixi = 'pixi', } export enum TerminalShellType { powershell = 'powershell', @@ -26,9 +28,10 @@ export enum TerminalShellType { fish = 'fish', cshell = 'cshell', tcshell = 'tshell', + nushell = 'nushell', wsl = 'wsl', xonsh = 'xonsh', - other = 'other' + other = 'other', } export interface ITerminalService extends IDisposable { @@ -47,9 +50,11 @@ export interface ITerminalService extends IDisposable { command: string, args: string[], cancel?: CancellationToken, - swallowExceptions?: boolean + swallowExceptions?: boolean, ): Promise; + /** @deprecated */ sendText(text: string): Promise; + executeCommand(commandLine: string, isPythonShell: boolean): Promise; show(preserveFocus?: boolean): Promise; } @@ -75,9 +80,9 @@ export type TerminalCreationOptions = { /** * Associated Python Interpreter. * - * @type {PythonInterpreter} + * @type {PythonEnvironment} */ - interpreter?: PythonInterpreter; + interpreter?: PythonEnvironment; /** * Whether hidden. * @@ -87,24 +92,11 @@ export type TerminalCreationOptions = { }; 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; + getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } @@ -117,12 +109,12 @@ export interface ITerminalHelper { getEnvironmentActivationCommands( terminalShellType: TerminalShellType, resource?: Uri, - interpreter?: PythonInterpreter + interpreter?: PythonEnvironment, ): Promise; getEnvironmentActivationShellCommands( resource: Resource, shell: TerminalShellType, - interpreter?: PythonInterpreter + interpreter?: PythonEnvironment, ): Promise; } @@ -130,12 +122,8 @@ export const ITerminalActivator = Symbol('ITerminalActivator'); export type TerminalActivationOptions = { resource?: Resource; preserveFocus?: boolean; - interpreter?: PythonInterpreter; - /** - * When sending commands to the terminal, do not display the terminal. - * - * @type {boolean} - */ + interpreter?: PythonEnvironment; + // When sending commands to the terminal, do not display the terminal. hideFromUser?: boolean; }; export interface ITerminalActivator { @@ -149,7 +137,7 @@ export interface ITerminalActivationCommandProvider { getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise; getActivationCommandsForInterpreter( pythonPath: string, - targetShell: TerminalShellType + targetShell: TerminalShellType, ): Promise; } @@ -159,7 +147,7 @@ export interface ITerminalActivationHandler { terminal: Terminal, resource: Uri | undefined, preserveFocus: boolean, - activated: boolean + activated: boolean, ): Promise; } @@ -169,16 +157,10 @@ 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 index 11ef8aaead62..c30ad704b6c1 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -1,52 +1,52 @@ // 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, + ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, Extension, ExtensionContext, - OutputChannel, + Memento, + LogOutputChannel, 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 type { InstallOptions, InterpreterUri, ModuleInstallFlags } from './installer/types'; import { EnvironmentVariables } from './variables/types'; -export const IOutputChannel = Symbol('IOutputChannel'); -export interface IOutputChannel extends OutputChannel {} +import { ITestingSettings } from '../testing/configuration/types'; + +export interface IDisposable { + dispose(): void | undefined | Promise; +} + +export const ILogOutputChannel = Symbol('ILogOutputChannel'); +export interface ILogOutputChannel extends LogOutputChannel {} 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 type IDisposableRegistry = IDisposable[]; 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 { + /** + * Storage is exposed in this type to make sure folks always use persistent state + * factory to access any type of storage as all storages are tracked there. + */ + readonly storage: Memento; 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]; @@ -64,52 +64,36 @@ export type ExecutionInfo = { moduleName?: string; args: string[]; product?: Product; + useShell?: boolean; }; export enum InstallerResponse { Installed, Disabled, - Ignore + Ignore, +} + +export enum ProductInstallStatus { + Installed, + NotInstalled, + NeedsUpgrade, } export enum ProductType { - Linter = 'Linter', - Formatter = 'Formatter', TestFramework = 'TestFramework', - RefactoringLibrary = 'RefactoringLibrary', - WorkspaceSymbols = 'WorkspaceSymbols', - DataScience = 'DataScience' + DataScience = 'DataScience', + Python = 'Python', } 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 + tensorboard = 24, + torchProfilerInstallName = 25, + torchProfilerImportName = 26, + pip = 27, + ensurepip = 28, + python = 29, } export const IInstaller = Symbol('IInstaller'); @@ -118,14 +102,25 @@ export interface IInstaller { promptToInstall( product: Product, resource?: InterpreterUri, - cancel?: CancellationToken + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise; + install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, ): Promise; - install(product: Product, resource?: InterpreterUri, cancel?: CancellationToken): Promise; - isInstalled(product: Product, resource?: InterpreterUri): Promise; - translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string; + isInstalled(product: Product, resource?: InterpreterUri): Promise; + isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise; + translateProductToModuleName(product: Product): 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'); @@ -155,150 +150,52 @@ export interface ICurrentProcess { readonly stdout: NodeJS.WriteStream; readonly stdin: NodeJS.ReadStream; readonly execPath: string; + // eslint-disable-next-line @typescript-eslint/ban-types on(event: string | symbol, listener: Function): this; } export interface IPythonSettings { + readonly interpreter: IInterpreterSettings; readonly pythonPath: string; readonly venvPath: string; readonly venvFolders: string[]; + readonly activeStateToolPath: 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 pixiToolPath: string; 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 languageServerIsDefault: boolean; readonly defaultInterpreterPath: string; - readonly logging: ILoggingSettings; -} -export interface ISortImportSettings { - readonly path: string; - readonly args: string[]; + readonly REPL: IREPLSettings; + register(): void; } -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 IInterpreterSettings { + infoVisibility: 'never' | 'onPythonRelated' | 'always'; } -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 focusAfterLaunch: boolean; readonly launchArgs: string[]; readonly activateEnvironment: boolean; readonly activateEnvInCurrentTerminal: boolean; + readonly shellIntegration: { + enabled: boolean; + }; +} + +export interface IREPLSettings { + readonly enableREPLSmartSend: boolean; + readonly sendToNativeREPL: boolean; } export interface IExperiments { @@ -316,111 +213,39 @@ export interface IExperiments { 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 openFilesOnly: boolean; - 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 interface IAutoCompleteSettings { + readonly extraPaths: string[]; } -export type InteractiveWindowMode = 'perFile' | 'single' | 'multiple'; - -export type WidgetCDNs = 'unpkg.com' | 'jsdelivr.com'; - export const IConfigurationService = Symbol('IConfigurationService'); export interface IConfigurationService { + readonly onDidChange: Event; getSettings(resource?: Uri): IPythonSettings; isTestExecution(): boolean; - updateSetting(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise; + updateSetting(setting: string, value?: unknown, resource?: Uri, configTarget?: ConfigurationTarget): Promise; updateSectionSetting( section: string, setting: string, - value?: {}, + value?: unknown, resource?: Uri, - configTarget?: ConfigurationTarget + configTarget?: ConfigurationTarget, ): Promise; } +/** + * Carries various tool execution path settings. For eg. pipenvPath, condaPath, pytestPath etc. These can be + * potentially used in discovery, autoselection, activation, installers, execution etc. And so should be a + * common interface to all the components. + */ +export const IToolExecutionPath = Symbol('IToolExecutionPath'); +export interface IToolExecutionPath { + readonly executable: string; +} +export enum ToolExecutionPath { + pipenv = 'pipenv', + // Gradually populate this list with tools as they come up. +} + export const ISocketServer = Symbol('ISocketServer'); export interface ISocketServer extends Disposable { readonly client: Promise; @@ -434,12 +259,6 @@ export type DownloadOptions = { * @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. * @@ -448,41 +267,6 @@ export type DownloadOptions = { 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 {} @@ -491,8 +275,8 @@ export interface IExtensions { /** * All extensions currently known to the system. */ - // tslint:disable-next-line:no-any - readonly all: readonly Extension[]; + + readonly all: readonly Extension[]; /** * An event which fires when `extensions.all` changes. This can happen when extensions are @@ -506,8 +290,8 @@ export interface IExtensions { * @param extensionId An extension identifier. * @return An extension or `undefined`. */ - // tslint:disable-next-line:no-any - getExtension(extensionId: string): Extension | undefined; + + getExtension(extensionId: string): Extension | undefined; /** * Get an extension its full identifier in the form of: `publisher.name`. @@ -516,6 +300,11 @@ export interface IExtensions { * @return An extension or `undefined`. */ getExtension(extensionId: string): Extension | undefined; + + /** + * Determines which extension called into our extension code based on call stacks. + */ + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>; } export const IBrowserService = Symbol('IBrowserService'); @@ -523,47 +312,6 @@ 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 */ @@ -572,71 +320,15 @@ export interface IHashFormat { 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 { + activate(): Promise; inExperiment(experimentName: string): Promise; + inExperimentSync(experimentName: string): boolean; + getExperimentValue(experimentName: string): Promise; } export type InterpreterConfigurationScope = { uri: Resource; configTarget: ConfigurationTarget }; @@ -655,5 +347,20 @@ export interface IInterpreterPathService { get(resource: Resource): string; inspect(resource: Resource): InspectInterpreterSettingType; update(resource: Resource, configTarget: ConfigurationTarget, value: string | undefined): Promise; - copyOldInterpreterStorageValuesToNew(resource: Uri | undefined): Promise; + copyOldInterpreterStorageValuesToNew(resource: Resource): Promise; +} + +export type DefaultLSType = LanguageServerType.Jedi | LanguageServerType.Node; + +/** + * Interface used to retrieve the default language server. + * + * Note: This is added to get around a problem that the config service is not `async`. + * Adding experiment check there would mean touching the entire extension. For simplicity + * this is a solution. + */ +export const IDefaultLanguageServer = Symbol('IDefaultLanguageServer'); + +export interface IDefaultLanguageServer { + readonly defaultLSType: DefaultLSType; } diff --git a/src/client/common/utils/arrayUtils.ts b/src/client/common/utils/arrayUtils.ts new file mode 100644 index 000000000000..5ec671118297 --- /dev/null +++ b/src/client/common/utils/arrayUtils.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Returns the elements of an array that meet the condition specified in an async callback function. + * @param asyncPredicate The filter method calls the async predicate function one time for each element in the array. + */ +export async function asyncFilter(arr: T[], asyncPredicate: (value: T) => Promise): Promise { + const results = await Promise.all(arr.map(asyncPredicate)); + return arr.filter((_v, index) => results[index]); +} + +export async function asyncForEach(arr: T[], asyncFunc: (value: T) => Promise): Promise { + await Promise.all(arr.map(asyncFunc)); +} diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 808ce8498b99..a44425f8f1a3 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-async-promise-executor */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -9,98 +11,90 @@ export async function sleep(timeout: number): Promise { }); } -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 +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types export function isThenable(v: any): v is Thenable { return typeof v?.then === 'function'; } -// tslint:disable-next-line: no-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 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; + reject(reason?: string | Error | Record | unknown): 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 _resolve!: (value: T | PromiseLike) => void; + + private _reject!: (reason?: string | Error | Record) => void; + + private _resolved = false; + + private _rejected = false; + private _promise: Promise; - // tslint:disable-next-line:no-any + + // eslint-disable-next-line @typescript-eslint/no-explicit-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); + + public resolve(_value: T | PromiseLike) { + if (this.completed) { + return; + } + this._resolve.apply(this.scope ? this.scope : this, [_value]); 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); + + public reject(_reason?: string | Error | Record) { + if (this.completed) { + return; + } + this._reject.apply(this.scope ? this.scope : this, [_reason]); 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 { + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +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 + // eslint-disable-next-line @typescript-eslint/no-explicit-any .then(deferred.resolve.bind(deferred) as any) - // tslint:disable-next-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any .catch(deferred.reject.bind(deferred) as any); return deferred; @@ -110,3 +104,190 @@ export function createDeferredFromPromise(promise: Promise): Deferred { promise.then(deferred.resolve.bind(deferred)).catch(deferred.reject.bind(deferred)); return deferred; } + +// iterators + +interface IAsyncIterator extends AsyncIterator {} + +export interface IAsyncIterableIterator extends IAsyncIterator, AsyncIterable {} + +/** + * An iterator that yields nothing. + */ +export function iterEmpty(): IAsyncIterableIterator { + return ((async function* () { + /** No body. */ + })() as unknown) as IAsyncIterableIterator; +} + +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: err as Error, result: null }; + } +} + +const NEVER: Promise = new Promise(() => { + /** No body. */ +}); + +/** + * 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. +): IAsyncIterableIterator { + const promises = iterators.map(getNext); + let numRunning = iterators.length; + + while (numRunning > 0) { + // Promise.race will not fail, because each promise calls getNext, + // Which handles failures by wrapping each iterator in a try/catch block. + 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; + } + } +} + +/** + * Map the async function onto the items and yield the results. + * + * @param items - the items to map onto and iterate + * @param func - the async function to apply for each item + * @param race - if `true` (the default) then results are yielded + * potentially out of order, as soon as each is ready + */ +export async function* mapToIterator( + items: T[], + func: (item: T) => Promise, + race = true, +): IAsyncIterableIterator { + if (race) { + const iterators = items.map((item) => { + async function* generator() { + yield func(item); + } + return generator(); + }); + yield* iterable(chain(iterators)); + } else { + yield* items.map(func); + } +} + +/** + * Convert an iterator into an iterable, if it isn't one already. + */ +export function iterable(iterator: IAsyncIterator): IAsyncIterableIterator { + const it = iterator as IAsyncIterableIterator; + if (it[Symbol.asyncIterator] === undefined) { + it[Symbol.asyncIterator] = () => it; + } + return it; +} + +/** + * Get everything yielded by the iterator. + */ +export async function flattenIterator(iterator: IAsyncIterator): Promise { + const results: T[] = []; + for await (const item of iterable(iterator)) { + results.push(item); + } + return results; +} + +/** + * Get everything yielded by the iterable. + */ +export async function flattenIterable(iterableItem: AsyncIterable): Promise { + const results: T[] = []; + for await (const item of iterableItem) { + results.push(item); + } + return results; +} + +/** + * Wait for a condition to be fulfilled within a timeout. + */ +export async function waitForCondition( + condition: () => Promise, + timeoutMs: number, + errorMessage: string, +): Promise { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + clearTimeout(timeout); + + clearTimeout(timer); + reject(new Error(errorMessage)); + }, timeoutMs); + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { + return; + } + clearTimeout(timeout); + clearTimeout(timer); + resolve(); + }, 10); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isPromiseLike(v: any): v is PromiseLike { + return typeof v?.then === 'function'; +} + +export function raceTimeout(timeout: number, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise { + const resolveValue = isPromiseLike(defaultValue) ? undefined : defaultValue; + if (isPromiseLike(defaultValue)) { + promises.push((defaultValue as unknown) as Promise); + } + + let promiseResolve: ((value: T) => void) | undefined = undefined; + + const timer = setTimeout(() => promiseResolve?.((resolveValue as unknown) as T), timeout); + + return Promise.race([ + Promise.race(promises).finally(() => clearTimeout(timer)), + new Promise((resolve) => (promiseResolve = resolve)), + ]); +} diff --git a/src/client/common/utils/cacheUtils.ts b/src/client/common/utils/cacheUtils.ts index 2f6895b4e5a8..6101b3ef928f 100644 --- a/src/client/common/utils/cacheUtils.ts +++ b/src/client/common/utils/cacheUtils.ts @@ -3,97 +3,9 @@ '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 - */ +// Gets a cache store to be used to store return values of methods or any other. export function getGlobalCacheStore() { return globalCacheStore; } @@ -105,18 +17,23 @@ export function getCacheKeyFromFunctionArgs(keyPrefix: string, fnArgs: any[]): s export function clearCache() { globalCacheStore.clear(); - resourceSpecificCacheStores.clear(); } +type CacheData = { + value: T; + expiry: number; +}; + +/** + * InMemoryCache caches a single value up until its expiry. + */ export class InMemoryCache { - private readonly _store = new Map(); - protected get store(): Map { - return this._store; - } - constructor(protected readonly expiryDurationMs: number, protected readonly cacheKey: string = '') {} + private cacheData?: CacheData; + + constructor(protected readonly expiryDurationMs: number) {} public get hasData() { - if (!this.store.get(this.cacheKey) || this.hasExpired(this.store.get(this.cacheKey)!.expiry)) { - this.store.delete(this.cacheKey); + if (!this.cacheData || this.hasExpired(this.cacheData.expiry)) { + this.cacheData = undefined; return false; } return true; @@ -130,19 +47,23 @@ export class InMemoryCache { * @memberof InMemoryCache */ public get data(): T | undefined { - if (!this.hasData || !this.store.has(this.cacheKey)) { + if (!this.hasData) { return; } - return this.store.get(this.cacheKey)?.value as T; + return this.cacheData?.value; } public set data(value: T | undefined) { - this.store.set(this.cacheKey, { - expiry: this.calculateExpiry(), - value - }); + if (value !== undefined) { + this.cacheData = { + expiry: this.calculateExpiry(), + value, + }; + } else { + this.cacheData = undefined; + } } public clear() { - this.store.clear(); + this.cacheData = undefined; } /** @@ -166,20 +87,3 @@ export class InMemoryCache { 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/charCode.ts b/src/client/common/utils/charCode.ts new file mode 100644 index 000000000000..ba76626bfcbb --- /dev/null +++ b/src/client/common/utils/charCode.ts @@ -0,0 +1,453 @@ +//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode' + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// 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, + + /** + * The   (no-break space) character. + * Unicode Character 'NO-BREAK SPACE' (U+00A0) + */ + NoBreakSpace = 160, + + 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 = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + 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_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300c, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300d, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + 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, + + U_FULLWIDTH_SEMICOLON = 0xff1b, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xff0c, // U+FF0C FULLWIDTH COMMA +} diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts index 512f31e65e19..44a82ee13760 100644 --- a/src/client/common/utils/decorators.ts +++ b/src/client/common/utils/decorators.ts @@ -1,16 +1,10 @@ -// 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 { traceError } from '../../logging'; 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'; +import { getCacheKeyFromFunctionArgs, getGlobalCacheStore } from './cacheUtils'; +import { StopWatch } from './stopWatch'; -// tslint:disable-next-line:no-require-imports no-var-requires const _debounce = require('lodash/debounce') as typeof import('lodash/debounce'); type VoidFunction = () => any; @@ -57,7 +51,6 @@ export function debounceAsync(wait?: number) { } 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 @@ -77,14 +70,13 @@ export function makeDebounceDecorator(wait?: number) { return originalMethod.apply(this, arguments as any); }, wait, - options + 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; @@ -127,25 +119,31 @@ export function makeDebounceAsyncDecorator(wait?: number) { }; } -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) { + +/** + * Extension start up time is considered the duration until extension is likely to keep running commands in background. + * It is observed on CI it can take upto 3 minutes, so this is an intelligent guess. + */ +const extensionStartUpTime = 200_000; +/** + * Tracks the time since the module was loaded. For caching purposes, we consider this time to approximately signify + * how long extension has been active. + */ +const moduleLoadWatch = new StopWatch(); +/** + * Caches function value until a specific duration. + * @param expiryDurationMs Duration to cache the result for. If set as '-1', the cache will never expire for the session. + * @param cachePromise If true, cache the promise instead of the promise result. + * @param expiryDurationAfterStartUpMs If specified, this is the duration to cache the result for after extension startup (until extension is likely to + * keep running commands in background) + */ +export function cache(expiryDurationMs: number, cachePromise = false, expiryDurationAfterStartUpMs?: number) { return function ( target: Object, propertyName: string, - descriptor: TypedPropertyDescriptor + descriptor: TypedPropertyDescriptor, ) { const originalMethod = descriptor.value!; const className = 'constructor' in target && target.constructor.name ? target.constructor.name : ''; @@ -154,18 +152,29 @@ export function cache(expiryDurationMs: number) { if (isTestExecution()) { return originalMethod.apply(this, args) as Promise; } - const key = getCacheKeyFromFunctionArgs(keyPrefix, args); + let key: string; + try { + key = getCacheKeyFromFunctionArgs(keyPrefix, args); + } catch (ex) { + traceError('Error while creating key for keyPrefix:', keyPrefix, ex); + return originalMethod.apply(this, args) as Promise; + } const cachedItem = cacheStoreForMethods.get(key); - if (cachedItem && cachedItem.expiry > Date.now()) { - traceVerbose(`Cached data exists ${key}`); + if (cachedItem && (cachedItem.expiry > Date.now() || expiryDurationMs === -1)) { return Promise.resolve(cachedItem.data); } + const expiryMs = + expiryDurationAfterStartUpMs && moduleLoadWatch.elapsedTime > extensionStartUpTime + ? expiryDurationAfterStartUpMs + : expiryDurationMs; const promise = originalMethod.apply(this, args) as Promise; - promise - .then((result) => - cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryDurationMs }) - ) - .ignoreErrors(); + if (cachePromise) { + cacheStoreForMethods.set(key, { data: promise, expiry: Date.now() + expiryMs }); + } else { + promise + .then((result) => cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryMs })) + .ignoreErrors(); + } return promise; }; }; @@ -178,15 +187,13 @@ export function cache(expiryDurationMs: number) { * @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 +export function swallowExceptions(scopeName?: string) { 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 + const errorMessage = `Python Extension (Error in ${scopeName || propertyName}, method:${propertyName}):`; + 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. @@ -207,56 +214,3 @@ export function swallowExceptions(scopeName: string) { }; }; } - -// 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/delayTrigger.ts b/src/client/common/utils/delayTrigger.ts new file mode 100644 index 000000000000..d110e005fc48 --- /dev/null +++ b/src/client/common/utils/delayTrigger.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { clearTimeout, setTimeout } from 'timers'; +import { Disposable } from 'vscode'; +import { traceVerbose } from '../../logging'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface IDelayedTrigger { + trigger(...args: any[]): void; +} + +/** + * DelayedTrigger can be used to prevent some action being called too + * often within a given duration. This was added to support file watching + * for tests. Suppose we are watching for *.py files. If the user installs + * a new package or runs a formatter on the entire workspace. This could + * trigger too many discover test calls which are expensive. We could + * debounce, but the limitation with debounce is that it might run before + * the package has finished installing. With delayed trigger approach + * we delay running until @param ms amount of time has passed. + */ +export class DelayedTrigger implements IDelayedTrigger, Disposable { + private timerId: NodeJS.Timeout | undefined; + + private triggeredCounter = 0; + + private calledCounter = 0; + + /** + * Delay calling the function in callback for a predefined amount of time. + * @param callback : Callback that should be called after some time has passed. + * @param ms : Amount of time after the last trigger that the call to callback + * should be delayed. + * @param name : A name for the callback action. This will be used in logs. + */ + constructor( + private readonly callback: (...args: any[]) => void, + private readonly ms: number, + private readonly name: string, + ) {} + + public trigger(...args: unknown[]): void { + this.triggeredCounter += 1; + if (this.timerId) { + clearTimeout(this.timerId); + } + + this.timerId = setTimeout(() => { + this.calledCounter += 1; + traceVerbose( + `Delay Trigger[${this.name}]: triggered=${this.triggeredCounter}, called=${this.calledCounter}`, + ); + this.callback(...args); + }, this.ms); + } + + public dispose(): void { + if (this.timerId) { + clearTimeout(this.timerId); + } + } +} diff --git a/src/client/common/utils/enum.ts b/src/client/common/utils/enum.ts index 2d6a53fdb505..78104b48846f 100644 --- a/src/client/common/utils/enum.ts +++ b/src/client/common/utils/enum.ts @@ -3,13 +3,11 @@ '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) { +function getNames(e: any) { return getObjValues(e).filter((v) => typeof v === 'string') as string[]; } diff --git a/src/client/common/utils/exec.ts b/src/client/common/utils/exec.ts new file mode 100644 index 000000000000..181934617eac --- /dev/null +++ b/src/client/common/utils/exec.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs'; +import * as path from 'path'; +import { getEnvironmentVariable, getOSType, OSType } from './platform'; + +/** + * Determine the env var to use for the executable search path. + */ +export function getSearchPathEnvVarNames(ostype = getOSType()): ('Path' | 'PATH')[] { + if (ostype === OSType.Windows) { + // On Windows both are supported now. + return ['Path', 'PATH']; + } + return ['PATH']; +} + +/** + * Get the OS executable lookup "path" from the appropriate env var. + */ +export function getSearchPathEntries(): string[] { + const envVars = getSearchPathEnvVarNames(); + for (const envVar of envVars) { + const value = getEnvironmentVariable(envVar); + if (value !== undefined) { + return parseSearchPathEntries(value); + } + } + // No env var was set. + return []; +} + +function parseSearchPathEntries(envVarValue: string): string[] { + return envVarValue + .split(path.delimiter) + .map((entry: string) => entry.trim()) + .filter((entry) => entry.length > 0); +} + +/** + * Determine if the given file is executable by the current user. + * + * If the file does not exist or has any other problem when accessed + * then `false` is returned. The caller is responsible to determine + * whether or not the file exists. + * + * If it could not be determined if the file is executable (e.g. on + * Windows) then `undefined` is returned. This allows the caller + * to decide what to do. + */ +export async function isValidAndExecutable(filename: string): Promise { + // There are three options when it comes to checking if a file + // is executable: `fs.stat()`, `fs.access()`, and + // `child_process.exec()`. `stat()` requires non-trivial logic + // to deal with user/group/everyone permissions. `exec()` requires + // that we make an attempt to actually execute the file, which is + // beyond the scope of this function (due to potential security + // risks). That leaves `access()`, which is what we use. + try { + // We do not need to check if the file exists. `fs.access()` + // takes care of that for us. + await fsapi.promises.access(filename, fsapi.constants.X_OK); + } catch (err) { + return false; + } + if (getOSType() === OSType.Windows) { + // On Windows a file is determined to be executable through + // its ACLs. However, the FS-related functionality provided + // by node does not check them (currently). This includes both + // `fs.stat()` and `fs.access()` (which we used above). One + // option is to use the "acl" NPM package (or something similar) + // to make the relevant checks. However, we want to avoid + // adding a dependency needlessly. Another option is to fall + // back to checking the filename's suffix (e.g. ".exe"). The + // problem there is that such a check is a bit *too* naive. + // Finally, we could still go the `exec()` route. We'd + // rather not given the concern identified above. Instead, + // it is good enough to return `undefined` and let the + // caller decide what to do about it. That is better + // than returning `true` when we aren't sure. + // + // Note that we still call `fs.access()` on Windows first, + // in case node makes it smarter later. + return undefined; + } + return true; +} diff --git a/src/client/common/utils/filesystem.ts b/src/client/common/utils/filesystem.ts new file mode 100644 index 000000000000..f2708e523bfb --- /dev/null +++ b/src/client/common/utils/filesystem.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as vscode from 'vscode'; +import { traceError } from '../../logging'; + +export import FileType = vscode.FileType; + +export type DirEntry = { + filename: string; + filetype: FileType; +}; + +interface IKnowsFileType { + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +} + +// 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(info: IKnowsFileType): FileType { + if (info.isFile()) { + return FileType.File; + } + if (info.isDirectory()) { + return FileType.Directory; + } + if (info.isSymbolicLink()) { + // The caller is responsible for combining this ("logical or") + // with File or Directory as necessary. + return FileType.SymbolicLink; + } + return FileType.Unknown; +} + +/** + * Identify the file type for the given file. + */ +export async function getFileType( + filename: string, + opts: { + ignoreErrors: boolean; + } = { ignoreErrors: true }, +): Promise { + let stat: fs.Stats; + try { + stat = await fs.promises.lstat(filename); + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code === 'ENOENT') { + return undefined; + } + if (opts.ignoreErrors) { + traceError(`lstat() failed for "${filename}" (${err})`); + return FileType.Unknown; + } + throw err; // re-throw + } + return convertFileType(stat); +} + +function normalizeFileTypes(filetypes: FileType | FileType[] | undefined): FileType[] | undefined { + if (filetypes === undefined) { + return undefined; + } + if (Array.isArray(filetypes)) { + if (filetypes.length === 0) { + return undefined; + } + return filetypes; + } + return [filetypes]; +} + +async function resolveFile( + file: string | DirEntry, + opts: { + ensure?: boolean; + onMissing?: FileType; + } = {}, +): Promise { + let filename: string; + if (typeof file !== 'string') { + if (!opts.ensure) { + if (opts.onMissing === undefined) { + return file; + } + // At least make sure it exists. + if ((await getFileType(file.filename)) !== undefined) { + return file; + } + } + filename = file.filename; + } else { + filename = file; + } + + const filetype = (await getFileType(filename)) || opts.onMissing; + if (filetype === undefined) { + return undefined; + } + return { filename, filetype }; +} + +type FileFilterFunc = (file: string | DirEntry) => Promise; + +export function getFileFilter( + opts: { + ignoreMissing?: boolean; + ignoreFileType?: FileType | FileType[]; + ensureEntry?: boolean; + } = { + ignoreMissing: true, + }, +): FileFilterFunc | undefined { + const ignoreFileType = normalizeFileTypes(opts.ignoreFileType); + + if (!opts.ignoreMissing && !ignoreFileType) { + // Do not filter. + return undefined; + } + + async function filterFile(file: string | DirEntry): Promise { + let entry = await resolveFile(file, { ensure: opts.ensureEntry }); + if (!entry) { + if (opts.ignoreMissing) { + return false; + } + const filename = typeof file === 'string' ? file : file.filename; + entry = { filename, filetype: FileType.Unknown }; + } + if (ignoreFileType) { + if (ignoreFileType.includes(entry!.filetype)) { + return false; + } + } + return true; + } + return filterFile; +} diff --git a/src/client/common/utils/icons.ts b/src/client/common/utils/icons.ts index 3d312818e058..71f71898ae9f 100644 --- a/src/client/common/utils/icons.ts +++ b/src/client/common/utils/icons.ts @@ -10,9 +10,9 @@ 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 } { +export function getIcon(fileName: string): { light: Uri; dark: Uri } { return { - dark: path.join(darkIconsPath, fileName), - light: path.join(lightIconsPath, fileName) + dark: Uri.file(path.join(darkIconsPath, fileName)), + light: Uri.file(path.join(lightIconsPath, fileName)), }; } diff --git a/src/client/common/utils/iterable.ts b/src/client/common/utils/iterable.ts new file mode 100644 index 000000000000..5e04aaa430ea --- /dev/null +++ b/src/client/common/utils/iterable.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace Iterable { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + export function is(thing: any): thing is Iterable { + return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function'; + } +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 43989b875772..7b7560c74e05 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -3,1382 +3,552 @@ 'use strict'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../constants'; -import { FileSystem } from '../platform/fileSystem'; +import { l10n } from 'vscode'; +import { Commands } from '../constants'; + +/* eslint-disable @typescript-eslint/no-namespace, no-shadow */ // 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 lsNotSupported = l10n.t( + 'Your operating system does not meet the minimum requirements of the Python Language Server. Reverting to the alternative autocompletion provider, Jedi.', ); - 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 = l10n.t( + 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Interpreter" in the status bar.', ); - 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 = l10n.t('The Python path in your debug configuration is invalid.'); + export const invalidDebuggerTypeDiagnostic = l10n.t( + '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 invalidPythonPathInDebuggerLaunch = localize( - 'diagnostics.invalidPythonPathInDebuggerLaunch', - 'The Python path in your debug configuration is invalid.' + export const consoleTypeDiagnostic = l10n.t( + '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 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 justMyCodeDiagnostic = l10n.t( + '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 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 yesUpdateLaunch = l10n.t('Yes, update launch.json'); + export const invalidTestSettings = l10n.t( + '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 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 updateSettings = l10n.t('Yes, update settings'); + export const pylanceDefaultMessage = l10n.t( + "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); - 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 invalidSmartSendMessage = l10n.t( + `Python is unable to parse the code provided. Please + turn off Smart Send if you wish to always run line by line or explicitly select code + to force run. See [logs](command:{0}) for more details`, + Commands.ViewOutput, ); - export const updateSettings = localize('diagnostics.updateSettings', 'Yes, update 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 const allow = l10n.t('Allow'); + export const seeInstructions = l10n.t('See Instructions'); + export const close = l10n.t('Close'); + export const bannerLabelYes = l10n.t('Yes'); + export const bannerLabelNo = l10n.t('No'); + export const canceled = l10n.t('Canceled'); + export const cancel = l10n.t('Cancel'); + export const ok = l10n.t('Ok'); + export const error = l10n.t('Error'); + export const gotIt = l10n.t('Got it!'); + export const install = l10n.t('Install'); + export const loadingExtension = l10n.t('Python extension loading...'); + export const openOutputPanel = l10n.t('Show output'); + export const noIWillDoItLater = l10n.t('No, I will do it later'); + export const notNow = l10n.t('Not now'); + export const doNotShowAgain = l10n.t("Don't show again"); + export const editSomething = l10n.t('Edit {0}'); + export const reload = l10n.t('Reload'); + export const moreInfo = l10n.t('More Info'); + export const learnMore = l10n.t('Learn more'); + export const and = l10n.t('and'); + export const reportThisIssue = l10n.t('Report this issue'); + export const recommended = l10n.t('Recommended'); + export const clearAll = l10n.t('Clear all'); + export const alwaysIgnore = l10n.t('Always Ignore'); + export const ignore = l10n.t('Ignore'); + export const selectPythonInterpreter = l10n.t('Select Python Interpreter'); + export const openLaunch = l10n.t('Open launch.json'); + export const useCommandPrompt = l10n.t('Use Command Prompt'); + export const download = l10n.t('Download'); + export const showLogs = l10n.t('Show logs'); + export const openFolder = l10n.t('Open Folder...'); } 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 const remindMeLaterLabel = l10n.t('Remind me later'); + export const yesLabel = l10n.t('Yes, take survey now'); + export const noLabel = l10n.t('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 const attachTitle = l10n.t('Attach to process'); + export const selectProcessPlaceholder = l10n.t('Select the process to attach to'); + export const noProcessSelected = l10n.t('No process selected'); + export const refreshList = l10n.t('Refresh process list'); } +export namespace Repl { + export const disableSmartSend = l10n.t('Disable Smart Send'); + export const launchNativeRepl = l10n.t('Launch VS Code Native REPL'); +} 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 remindMeLater = l10n.t('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 pylanceNotInstalledMessage = l10n.t('Pylance extension is not installed.'); + export const pylanceInstalledReloadPromptMessage = l10n.t( + 'Pylance extension is now installed. Reload window to activate?', ); - export const pylanceInstalledReloadPromptMessage = localize( - 'Pylance.pylanceInstalledReloadPromptMessage', - 'Pylance extension is now installed. Reload window to activate?' + + export const pylanceRevertToJediPrompt = l10n.t( + 'The Pylance extension is not installed but the python.languageServer value is set to "Pylance". Would you like to install the Pylance extension to use Pylance, or revert back to Jedi?', ); + export const pylanceInstallPylance = l10n.t('Install Pylance'); + export const pylanceRevertToJedi = l10n.t('Revert to Jedi'); } -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 namespace TensorBoard { + export const enterRemoteUrl = l10n.t('Enter remote URL'); + export const enterRemoteUrlDetail = l10n.t( + 'Enter a URL pointing to a remote directory containing your TensorBoard log files', ); - 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 useCurrentWorkingDirectoryDetail = l10n.t( + 'TensorBoard will search for tfevent files in all subdirectories of the current working directory', ); - - 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 useCurrentWorkingDirectory = l10n.t('Use current working directory'); + export const logDirectoryPrompt = l10n.t('Select a log directory to start TensorBoard with'); + export const progressMessage = l10n.t('Starting TensorBoard session...'); + export const nativeTensorBoardPrompt = l10n.t( + 'VS Code now has integrated TensorBoard support. Would you like to launch TensorBoard? (Tip: Launch TensorBoard anytime by opening the command palette and searching for "Launch TensorBoard".)', ); - 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 selectAFolder = l10n.t('Select a folder'); + export const selectAFolderDetail = l10n.t('Select a log directory containing tfevent files'); + export const selectAnotherFolder = l10n.t('Select another folder'); + export const selectAnotherFolderDetail = l10n.t('Use the file explorer to select another folder'); + export const installPrompt = l10n.t( + 'The package TensorBoard is required to launch a TensorBoard session. Would you like to install it?', ); - 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 installTensorBoardAndProfilerPluginPrompt = l10n.t( + 'TensorBoard >= 2.4.1 and the PyTorch Profiler TensorBoard plugin >= 0.2.0 are required. Would you like to install these packages?', ); - export const downloadFailedOutputMessage = localize( - 'LanguageService.downloadFailedOutputMessage', - 'Language server download failed.' + export const installProfilerPluginPrompt = l10n.t( + 'We recommend installing version >= 0.2.0 of the PyTorch Profiler TensorBoard plugin. Would you like to install the package?', ); - export const extractionFailedOutputMessage = localize( - 'LanguageService.extractionFailedOutputMessage', - 'Language server extraction failed.' + export const upgradePrompt = l10n.t( + 'Integrated TensorBoard support is only available for TensorBoard >= 2.4.1. Would you like to upgrade your copy of TensorBoard?', ); - export const extractionCompletedOutputMessage = localize( - 'LanguageService.extractionCompletedOutputMessage', - 'Language server download complete.' + export const missingSourceFile = l10n.t( + 'The Python extension could not locate the requested source file on disk. Please manually specify the file.', ); - 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 const selectMissingSourceFile = l10n.t('Choose File'); + export const selectMissingSourceFileDescription = l10n.t( + "The source file's contents may not match the original contents in the trace.", ); } -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 namespace LanguageService { + export const virtualWorkspaceStatusItem = { + detail: l10n.t('Limited IntelliSense supported by Jedi and Pylance'), }; - 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 const statusItem = { + name: l10n.t('Python IntelliSense Status'), + text: l10n.t('Partial Mode'), + detail: l10n.t('Limited IntelliSense provided by Pylance'), }; -} -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 startingPylance = l10n.t('Starting Pylance language server.'); + export const startingNone = l10n.t('Editor support is inactive since language server is set to None.'); + export const untrustedWorkspaceMessage = l10n.t( + 'Only Pylance is supported in untrusted workspaces, setting language server to None.', ); - 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 const reloadAfterLanguageServerChange = l10n.t( + 'Reload the window after switching between language servers.', ); -} - -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 lsFailedToStart = l10n.t( + 'We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.', ); - 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 lsFailedToDownload = l10n.t( + 'We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.', ); - export const startingDownloadOutputMessage = localize( - 'DataScienceRendererExtension.startingDownloadOutputMessage', - 'Starting download of Notebook Renderers extension.' + export const lsFailedToExtract = l10n.t( + 'We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.', ); - export const downloadingMessage = localize( - 'DataScienceRendererExtension.downloadingMessage', - 'Downloading Notebook Renderers Extension... ' - ); - export const downloadCompletedOutputMessage = localize( - 'DataScienceRendererExtension.downloadCompletedOutputMessage', - 'Notebook Renderers extension download complete.' + export const downloadFailedOutputMessage = l10n.t('Language server download failed.'); + export const extractionFailedOutputMessage = l10n.t('Language server extraction failed.'); + export const extractionCompletedOutputMessage = l10n.t('Language server download complete.'); + export const extractionDoneOutputMessage = l10n.t('done.'); + export const reloadVSCodeIfSeachPathHasChanged = l10n.t( + 'Search paths have changed for this Python interpreter. Reload the extension to ensure that the IntelliSense works correctly.', ); } -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/gathersurvey\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 fallbackToUseActiveInterpeterAsKernel = 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 namespace Interpreters { + export const requireJupyter = l10n.t( + 'Running in Interactive window requires Jupyter Extension. Would you like to install it? [Learn more](https://aka.ms/pythonJupyterSupport).', ); - 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 installingPython = l10n.t('Installing Python into Environment...'); + export const discovering = l10n.t('Discovering Python Interpreters'); + export const refreshing = l10n.t('Refreshing Python Interpreters'); + export const envExtDiscoveryAttribution = l10n.t( + 'Environment discovery is managed by the Python Environments extension (ms-python.vscode-python-envs). Check the "Python Environments" output channel for environment-specific logs.', ); - export const jupyterCommandLineCustomLabel = localize('DataScience.jupyterCommandLineCustomLabel', 'Custom'); - export const jupyterCommandLineCustomDetail = localize( - 'DataScience.jupyterCommandLineCustomDetail', - 'Customize the command line passed to Jupyter on startup' + export const envExtDiscoveryFailed = l10n.t( + 'Environment discovery failed. Check the "Python Environments" output channel for details. The Python Environments extension (ms-python.vscode-python-envs) manages environment discovery.', ); - export const jupyterCommandLineReloadQuestion = localize( - 'DataScience.jupyterCommandLineReloadQuestion', - 'Please reload the window when changing the Jupyter command line.' + export const envExtDiscoverySlow = l10n.t( + 'Environment discovery is taking longer than expected. Check the "Python Environments" output channel for progress. The Python Environments extension (ms-python.vscode-python-envs) manages environment discovery.', ); - export const jupyterCommandLineReloadAnswer = localize('DataScience.jupyterCommandLineReloadAnswer', 'Reload'); - export const jupyterCommandLineQuickPickPlaceholder = localize( - 'DataScience.jupyterCommandLineQuickPickPlaceholder', - 'Choose an option' + export const envExtActivationFailed = l10n.t( + 'Failed to activate the Python Environments extension (ms-python.vscode-python-envs), which is required for environment discovery. Please ensure it is installed and enabled.', ); - export const jupyterCommandLineQuickPickTitle = localize( - 'DataScience.jupyterCommandLineQuickPickTitle', - 'Pick command line for Jupyter' + export const envExtDiscoveryNoEnvironments = l10n.t( + 'Environment discovery completed but no Python environments were found. Check the "Python Environments" output channel for details.', ); - export const jupyterCommandLinePrompt = localize( - 'DataScience.jupyterCommandLinePrompt', - 'Enter your custom command line for Jupyter' + export const envExtNoActiveEnvironment = l10n.t( + 'No Python environment is set for this resource. Check the "Python Environments" output channel for details, or select an interpreter.', ); - - export const connectingToJupyterUri = localize( - 'DataScience.connectingToJupyterUri', - 'Connecting to Jupyter server at {0}' + export const condaInheritEnvMessage = l10n.t( + '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. [Learn more](https://aka.ms/AA66i8f).', ); - 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 activatingTerminals = l10n.t('Reactivating terminals...'); + export const activateTerminalDescription = l10n.t('Activated environment for'); + export const terminalEnvVarCollectionPrompt = l10n.t( + '{0} environment was successfully activated, even though {1} indicator may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); - - 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 shellIntegrationEnvVarCollectionDescription = l10n.t( + 'Enables `python.terminal.shellIntegration.enabled` by modifying `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', ); - - 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 shellIntegrationDisabledEnvVarCollectionDescription = l10n.t( + 'Disables `python.terminal.shellIntegration.enabled` by unsetting `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', ); - export const jupyterSelectURIRemoteDetail = localize( - 'DataScience.jupyterSelectURIRemoteDetail', - 'Specify the URI of an existing server' + export const terminalDeactivateProgress = l10n.t('Editing {0}...'); + export const restartingTerminal = l10n.t('Restarting terminal and deactivating...'); + export const terminalDeactivatePrompt = l10n.t( + 'Deactivating virtual environments may not work by default. To make it work, edit your "{0}" and then restart your shell. [Learn more](https://aka.ms/AAmx2ft).', ); - - export const loadClassFailedWithNoInternet = localize( - 'DataScience.loadClassFailedWithNoInternet', - 'Error loading {0}:{1}. Internet connection required for loading 3rd party widgets.' + export const activatedCondaEnvLaunch = l10n.t( + 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); - export const loadThirdPartyWidgetScriptsPostEnabled = localize( - 'DataScience.loadThirdPartyWidgetScriptsPostEnabled', - "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'." + export const environmentPromptMessage = l10n.t( + 'We noticed a new environment has been created. Do you want to select it for the workspace folder?', ); - 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 entireWorkspace = l10n.t('Select at workspace level'); + export const clearAtWorkspace = l10n.t('Clear at workspace level'); + export const selectInterpreterTip = l10n.t( + 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar', ); - 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 installPythonTerminalMessageLinux = l10n.t( + '💡 Try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', ); - export const unhandledMessage = localize( - 'DataScience.unhandledMessage', - 'Unhandled kernel message from a widget: {0} : {1}' + export const installPythonTerminalMacMessage = l10n.t( + '💡 Brew does not seem to be available. You can download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', ); + export const changePythonInterpreter = l10n.t('Change Python Interpreter'); + export const selectedPythonInterpreter = l10n.t('Selected Python Interpreter'); +} - 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 namespace InterpreterQuickPickList { + export const condaEnvWithoutPythonTooltip = l10n.t( + 'Python is not available in this environment, it will automatically be installed upon selecting it', ); + export const noPythonInstalled = l10n.t('Python is not installed'); + export const clickForInstructions = l10n.t('Click for instructions...'); + export const globalGroupName = l10n.t('Global'); + export const workspaceGroupName = l10n.t('Workspace'); + export const enterPath = { + label: l10n.t('Enter interpreter path...'), + placeholder: l10n.t('Enter path to a Python interpreter.'), + }; + export const defaultInterpreterPath = { + label: l10n.t('Use Python from `python.defaultInterpreterPath` setting'), + }; + export const browsePath = { + label: l10n.t('Find...'), + detail: l10n.t('Browse your file system to find a Python interpreter.'), + openButtonLabel: l10n.t('Select Interpreter'), + title: l10n.t('Select Python interpreter'), + }; + export const refreshInterpreterList = l10n.t('Refresh Interpreter list'); + export const refreshingInterpreterList = l10n.t('Refreshing Interpreter list...'); + export const create = { + label: l10n.t('Create Virtual Environment...'), + }; +} - 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 namespace OutputChannelNames { + export const languageServer = l10n.t('Python Language Server'); + export const python = l10n.t('Python'); +} - 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 namespace Linters { + export const selectLinter = l10n.t('Select Linter'); } -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 namespace Installer { + export const noCondaOrPipInstaller = l10n.t( + 'There is no Conda or Pip installer available in the selected environment.', ); + export const noPipInstaller = l10n.t('There is no Pip installer available in the selected environment.'); + export const searchForHelp = l10n.t('Search for help'); +} - 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 ExtensionSurveyBanner { + export const bannerMessage = l10n.t( + 'Can you take 2 minutes to tell us how the Python extension is working for you?', ); + export const bannerLabelYes = l10n.t('Yes, take survey now'); + export const bannerLabelNo = l10n.t('No, thanks'); + export const maybeLater = l10n.t('Maybe later'); } - export namespace DebugConfigStrings { export const selectConfiguration = { - title: localize('debug.selectConfigurationTitle'), - placeholder: localize('debug.selectConfigurationPlaceholder') + title: l10n.t('Select a debug configuration'), + placeholder: l10n.t('Debug Configuration'), }; export const launchJsonCompletions = { - label: localize('debug.launchJsonConfigurationsCompletionLabel'), - description: localize('debug.launchJsonConfigurationsCompletionDescription') + label: l10n.t('Python'), + description: l10n.t('Select a Python debug configuration'), }; export namespace file { export const snippet = { - name: localize('python.snippet.launch.standard.label') + name: l10n.t('Python: Current File'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugFileConfigurationLabel'), - description: localize('debug.debugFileConfigurationDescription') + label: l10n.t('Python File'), + description: l10n.t('Debug the currently active Python file'), }; } export namespace module { export const snippet = { - name: localize('python.snippet.launch.module.label'), - default: localize('python.snippet.launch.module.default') + name: l10n.t('Python: Module'), + default: l10n.t('enter-your-module-name'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugModuleConfigurationLabel'), - description: localize('debug.debugModuleConfigurationDescription') + label: l10n.t('Module'), + description: l10n.t("Debug a Python module by invoking it with '-m'"), }; export const enterModule = { - title: localize('debug.moduleEnterModuleTitle'), - prompt: localize('debug.moduleEnterModulePrompt'), - default: localize('debug.moduleEnterModuleDefault'), - invalid: localize('debug.moduleEnterModuleInvalidNameError') + title: l10n.t('Debug Module'), + prompt: l10n.t('Enter a Python module/package name'), + default: l10n.t('enter-your-module-name'), + invalid: l10n.t('Enter a valid module name'), }; } export namespace attach { export const snippet = { - name: localize('python.snippet.launch.attach.label') + name: l10n.t('Python: Remote Attach'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.remoteAttachConfigurationLabel'), - description: localize('debug.remoteAttachConfigurationDescription') + label: l10n.t('Remote Attach'), + description: l10n.t('Attach to a remote debug server'), }; export const enterRemoteHost = { - title: localize('debug.attachRemoteHostTitle'), - prompt: localize('debug.attachRemoteHostPrompt'), - invalid: localize('debug.attachRemoteHostValidationError') + title: l10n.t('Remote Debugging'), + prompt: l10n.t('Enter a valid host name or IP address'), + invalid: l10n.t('Enter a valid host name or IP address'), }; export const enterRemotePort = { - title: localize('debug.attachRemotePortTitle'), - prompt: localize('debug.attachRemotePortPrompt'), - invalid: localize('debug.attachRemotePortValidationError') + title: l10n.t('Remote Debugging'), + prompt: l10n.t('Enter the port number that the debug server is listening on'), + invalid: l10n.t('Enter a valid port number'), }; } export namespace attachPid { export const snippet = { - name: localize('python.snippet.launch.attachpid.label') + name: l10n.t('Python: Attach using Process Id'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.attachPidConfigurationLabel'), - description: localize('debug.attachPidConfigurationDescription') + label: l10n.t('Attach using Process ID'), + description: l10n.t('Attach to a local process'), }; } export namespace django { export const snippet = { - name: localize('python.snippet.launch.django.label') + name: l10n.t('Python: Django'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugDjangoConfigurationLabel'), - description: localize('debug.debugDjangoConfigurationDescription') + label: l10n.t('Django'), + description: l10n.t('Launch and debug a Django web application'), }; export const enterManagePyPath = { - title: localize('debug.djangoEnterManagePyPathTitle'), - prompt: localize('debug.djangoEnterManagePyPathPrompt'), - invalid: localize('debug.djangoEnterManagePyPathInvalidFilePathError') + title: l10n.t('Debug Django'), + prompt: l10n.t( + "Enter the path to manage.py ('${workspaceFolder}' points to the root of the current workspace folder)", + ), + invalid: l10n.t('Enter a valid Python file path'), + }; + } + export namespace fastapi { + export const snippet = { + name: l10n.t('Python: FastAPI'), + }; + + export const selectConfiguration = { + label: l10n.t('FastAPI'), + description: l10n.t('Launch and debug a FastAPI web application'), + }; + export const enterAppPathOrNamePath = { + title: l10n.t('Debug FastAPI'), + prompt: l10n.t("Enter the path to the application, e.g. 'main.py' or 'main'"), + invalid: l10n.t('Enter a valid name'), }; } export namespace flask { export const snippet = { - name: localize('python.snippet.launch.flask.label') + name: l10n.t('Python: Flask'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugFlaskConfigurationLabel'), - description: localize('debug.debugFlaskConfigurationDescription') + label: l10n.t('Flask'), + description: l10n.t('Launch and debug a Flask web application'), }; export const enterAppPathOrNamePath = { - title: localize('debug.flaskEnterAppPathOrNamePathTitle'), - prompt: localize('debug.flaskEnterAppPathOrNamePathPrompt'), - invalid: localize('debug.flaskEnterAppPathOrNamePathInvalidNameError') + title: l10n.t('Debug Flask'), + prompt: l10n.t('Python: Flask'), + invalid: l10n.t('Enter a valid name'), }; } export namespace pyramid { export const snippet = { - name: localize('python.snippet.launch.pyramid.label') + name: l10n.t('Python: Pyramid Application'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugPyramidConfigurationLabel'), - description: localize('debug.debugPyramidConfigurationDescription') + label: l10n.t('Pyramid'), + description: l10n.t('Launch and debug a Pyramid web application'), }; export const enterDevelopmentIniPath = { - title: localize('debug.pyramidEnterDevelopmentIniPathTitle'), - prompt: localize('debug.pyramidEnterDevelopmentIniPathPrompt'), - invalid: localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError') + title: l10n.t('Debug Pyramid'), + invalid: l10n.t('Enter a valid file path'), }; } } 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 const configureTests = l10n.t('Configure Test Framework'); + export const cancelUnittestDiscovery = l10n.t('Canceled unittest test discovery'); + export const errorUnittestDiscovery = l10n.t('Unittest test discovery error'); + export const cancelPytestDiscovery = l10n.t('Canceled pytest test discovery'); + export const errorPytestDiscovery = l10n.t('pytest test discovery error'); + export const seePythonOutput = l10n.t('(see Output > Python)'); + export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); + export const errorUnittestExecution = l10n.t('Unittest test execution error'); + export const cancelPytestExecution = l10n.t('Canceled pytest test execution'); + export const errorPytestExecution = l10n.t('pytest test execution error'); + export const copilotSetupMessage = l10n.t('Confirm your Python testing framework to enable test discovery.'); } 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).' + export const outdatedDebuggerMessage = l10n.t( + 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Use [debugpy](https://aka.ms/migrateToDebugpy) instead.', ); } -// 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 = {}; +export namespace Python27Support { + export const jediMessage = l10n.t( + 'IntelliSense with Jedi for Python 2.7 is no longer supported. [Learn more](https://aka.ms/python-27-support).', + ); } -// This is exported only for testing purposes. -export function _getAskedForCollection() { - return askedForCollection; +export namespace SwitchToDefaultLS { + export const bannerMessage = l10n.t( + "The Microsoft Python Language Server has reached end of life. Your language server has been set to the default for Python in VS Code, Pylance.\n\nIf you'd like to change your language server, you can learn about how to do so [here](https://devblogs.microsoft.com/python/python-in-visual-studio-code-may-2021-release/#configuring-your-language-server).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", + ); } -// 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 }); -} +export namespace CreateEnv { + export const informEnvCreation = l10n.t('The following environment is selected:'); + export const statusTitle = l10n.t('Creating environment'); + export const statusStarting = l10n.t('Starting...'); -// 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); - }; -} + export const hasVirtualEnv = l10n.t('Workspace folder contains a virtual environment'); -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'; -} + export const noWorkspace = l10n.t('A workspace is required when creating an environment using venv.'); -function getString(key: string, defValue?: string) { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } + export const pickWorkspacePlaceholder = l10n.t('Select a workspace to create environment'); - // 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!; + export const providersQuickPickPlaceholder = l10n.t('Select an environment type'); - // Use the current locale if the key is defined there. - if (loadedCollection && loadedCollection.hasOwnProperty(key)) { - collection = loadedCollection; + export namespace Venv { + export const creating = l10n.t('Creating venv...'); + export const creatingMicrovenv = l10n.t('Creating microvenv...'); + export const created = l10n.t('Environment created...'); + export const existing = l10n.t('Using existing environment...'); + export const downloadingPip = l10n.t('Downloading pip...'); + export const installingPip = l10n.t('Installing pip...'); + export const upgradingPip = l10n.t('Upgrading pip...'); + export const installingPackages = l10n.t('Installing packages...'); + export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.'); + export const selectPythonPlaceHolder = l10n.t('Select a Python installation to create the virtual environment'); + export const providerDescription = l10n.t('Creates a `.venv` virtual environment in the current workspace'); + export const error = l10n.t('Creating virtual environment failed with error.'); + export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); + export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t( + 'Delete existing ".venv" directory and create a new ".venv" environment', + ); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); + export const existingVenvQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".venv" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); + export const openRequirementsFile = l10n.t('Open requirements file'); } - 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 = {}; + export namespace Conda { + export const condaMissing = l10n.t('Install `conda` to create conda environments.'); + export const created = l10n.t('Environment created...'); + export const installingPackages = l10n.t('Installing packages...'); + export const errorCreatingEnvironment = l10n.t('Error while creating conda environment.'); + export const selectPythonQuickPickPlaceholder = l10n.t( + 'Select the version of Python to install in the environment', + ); + export const creating = l10n.t('Creating conda environment...'); + export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); + + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t('Delete existing ".conda" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".conda" environment with no changes to it'); + export const existingCondaQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".conda" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); } - // 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 = {}; - } + export namespace Trigger { + export const workspaceTriggerMessage = l10n.t( + 'A virtual environment is not currently selected for your Python interpreter. Would you like to create a virtual environment?', + ); + export const createEnvironment = l10n.t('Create'); + + export const globalPipInstallTriggerMessage = l10n.t( + 'You may have installed Python packages into your global environment, which can cause conflicts between package versions. Would you like to create a virtual environment with these packages to isolate your dependencies?', + ); } } -// Default to loading the current locale -load(); +export namespace PythonLocator { + export const startupFailedNotification = l10n.t( + 'Python Locator failed to start. Python environment discovery may not work correctly.', + ); + export const windowsRuntimeMissing = l10n.t( + 'Missing Windows runtime dependencies detected. The Python Locator requires the Microsoft Visual C++ Redistributable. This is often missing on clean Windows installations.', + ); + export const windowsStartupFailed = l10n.t( + 'Python Locator failed to start on Windows. This might be due to missing system dependencies such as the Microsoft Visual C++ Redistributable.', + ); +} diff --git a/src/client/common/utils/logging.ts b/src/client/common/utils/logging.ts deleted file mode 100644 index c9c2f756c094..000000000000 --- a/src/client/common/utils/logging.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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 index fbfe65d5df2a..a461d25d9d30 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -2,93 +2,31 @@ // Licensed under the MIT License. 'use strict'; import type { TextDocument, Uri } from 'vscode'; -import { NotebookCellScheme } from '../constants'; +import { InteractiveInputScheme, NotebookCellScheme } from '../constants'; import { InterpreterUri } from '../installer/types'; -import { IAsyncDisposable, IDisposable, Resource } from '../types'; -import { isPromise } from './async'; -import { StopWatch } from './stopWatch'; +import { isParentPath } from '../platform/fs-paths'; +import { Resource } from '../types'; -// tslint:disable-next-line:no-empty export function noop() {} /** - * Execute a block of code ignoring any exceptions. + * Like `Readonly<>`, but recursive. + * + * See https://github.com/Microsoft/TypeScript/pull/21316. */ -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(); - } -} -// 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; +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; }; - -// 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; - } -} +type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; /** * 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) { @@ -102,13 +40,9 @@ export function isResource(resource?: InterpreterUri): resource is Resource { * 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 { + +function isUri(resource?: Uri | any): resource is Uri { if (!resource) { return false; } @@ -116,11 +50,50 @@ export function isUri(resource?: Uri | any): resource is 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); +/** + * Create a filter func that determine if the given URI and candidate match. + * + * Only compares path. + * + * @param checkParent - if `true`, match if the candidate is rooted under `uri` + * or if the candidate matches `uri` exactly. + * @param checkChild - if `true`, match if `uri` is rooted under the candidate + * or if the candidate matches `uri` exactly. + */ +export function getURIFilter( + uri: Uri, + opts: { + checkParent?: boolean; + checkChild?: boolean; + } = { checkParent: true }, +): (u: Uri) => boolean { + let uriPath = uri.path; + while (uriPath.endsWith('/')) { + uriPath = uriPath.slice(0, -1); + } + const uriRoot = `${uriPath}/`; + function filter(candidate: Uri): boolean { + // Do not compare schemes as it is sometimes not available, in + // which case file is assumed as scheme. + let candidatePath = candidate.path; + while (candidatePath.endsWith('/')) { + candidatePath = candidatePath.slice(0, -1); + } + if (opts.checkParent && isParentPath(candidatePath, uriRoot)) { + return true; + } + if (opts.checkChild) { + const candidateRoot = `${candidatePath}/`; + if (isParentPath(uriPath, candidateRoot)) { + return true; + } + } + return false; + } + return filter; } -export function isUntitledFile(file?: Uri) { - return file?.scheme === 'untitled'; +export function isNotebookCell(documentOrUri: TextDocument | Uri): boolean { + const uri = isUri(documentOrUri) ? documentOrUri : documentOrUri.uri; + return uri.scheme.includes(NotebookCellScheme) || uri.scheme.includes(InteractiveInputScheme); } diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index 03e68155e86a..2de1684a4d2e 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -1,43 +1,71 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; +/* eslint-disable max-classes-per-file */ -// tslint:disable:max-func-body-length no-any no-unnecessary-class +'use strict'; import { inject, injectable } from 'inversify'; -import { Disposable, QuickInput, QuickInputButton, QuickInputButtons, QuickPickItem } from 'vscode'; +import { Disposable, QuickInput, QuickInputButton, QuickInputButtons, QuickPick, QuickPickItem, Event } from 'vscode'; import { IApplicationShell } from '../application/types'; +import { createDeferred } from './async'; // 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() {} + + private constructor() { + /** No body. */ + } } -export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; + +type buttonCallbackType = (quickPick: QuickPick) => void; -export interface IQuickPickParameters { +export type QuickInputButtonSetup = { + /** + * Button for an action in a QuickPick. + */ + button: QuickInputButton; + /** + * Callback to be invoked when button is clicked. + */ + callback: buttonCallbackType; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IQuickPickParameters { title?: string; step?: number; totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T; - placeholder: string; - buttons?: QuickInputButton[]; + activeItem?: T | ((quickPick: QuickPick) => Promise); + placeholder: string | undefined; + customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; matchOnDetail?: boolean; + keepScrollPosition?: boolean; + sortByLabel?: boolean; acceptFilterBoxTextAsSelection?: boolean; - shouldResume?(): Promise; + /** + * A method called only after quickpick has been created and all handlers are registered. + */ + initialize?: (quickPick: QuickPick) => void; + onChangeItem?: { + callback: (event: E, quickPick: QuickPick) => void; + event: Event; + }; } -// tslint:disable-next-line: interface-name -export interface InputBoxParameters { +interface InputBoxParameters { title: string; password?: boolean; step?: number; @@ -46,11 +74,10 @@ export interface InputBoxParameters { 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); +type MultiStepInputQuickPickResponseType = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined; +type MultiStepInputInputBoxResponseType

= string | (P extends { buttons: (infer I)[] } ? I : never) | undefined; export interface IMultiStepInput { run(start: InputStep, state: S): Promise; showQuickPick>({ @@ -60,9 +87,8 @@ export interface IMultiStepInput { items, activeItem, placeholder, - buttons, - shouldResume - }: P): Promise>; + customButtonSetups, + }: P): Promise>; showInputBox

({ title, step, @@ -71,15 +97,17 @@ export interface IMultiStepInput { 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) { + + public run(start: InputStep, state: S): Promise { return this.stepThrough(start, state); } @@ -90,60 +118,94 @@ export class MultiStepInput implements IMultiStepInput { items, activeItem, placeholder, - buttons, - shouldResume, + customButtonSetups, matchOnDescription, matchOnDetail, - acceptFilterBoxTextAsSelection - }: P): Promise> { + acceptFilterBoxTextAsSelection, + onChangeItem, + keepScrollPosition, + sortByLabel, + initialize, + }: 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 = []; + const input = this.shell.createQuickPick(); + input.title = title; + input.step = step; + input.sortByLabel = sortByLabel || false; + input.totalSteps = totalSteps; + input.placeholder = placeholder; + input.ignoreFocusOut = true; + input.items = items; + input.matchOnDescription = matchOnDescription || false; + input.matchOnDetail = matchOnDetail || false; + input.buttons = this.steps.length > 1 ? [QuickInputButtons.Back] : []; + if (customButtonSetups) { + for (const customButtonSetup of customButtonSetups) { + input.buttons = [...input.buttons, customButtonSetup.button]; + } + } + if (this.current) { + this.current.dispose(); + } + this.current = input; + if (onChangeItem) { + disposables.push(onChangeItem.event((e) => onChangeItem.callback(e, input))); + } + // Quickpick should be initialized synchronously and on changed item handlers are registered synchronously. + if (initialize) { + initialize(input); + } + if (activeItem) { + if (typeof activeItem === 'function') { + activeItem(input).then((item) => { + if (input.activeItems.length === 0) { + input.activeItems = [item]; + } + }); + } + } else { + input.activeItems = []; + } + this.current.show(); + // Keep scroll position is only meant to keep scroll position when updating items, + // so do it after initialization. This ensures quickpick starts with the active + // item in focus when this is true, instead of having scroll position at top. + input.keepScrollPosition = keepScrollPosition; + + const deferred = createDeferred(); + + disposables.push( + input.onDidTriggerButton(async (item) => { + if (item === QuickInputButtons.Back) { + deferred.reject(InputFlowAction.back); + input.hide(); } - input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])]; - disposables.push( - input.onDidTriggerButton((item) => { - if (item === QuickInputButtons.Back) { - reject(InputFlowAction.back); - } else { - resolve(item); + if (customButtonSetups) { + for (const customButtonSetup of customButtonSetups) { + if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) { + await customButtonSetup?.callback(input); } - }), - 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(); + }), + input.onDidChangeSelection((selectedItems) => deferred.resolve(selectedItems[0])), + input.onDidHide(() => { + if (!deferred.completed) { + deferred.resolve(undefined); } - this.current = input; - this.current.show(); - }); + }), + ); + if (acceptFilterBoxTextAsSelection) { + disposables.push( + input.onDidAccept(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deferred.resolve(input.value as any); + }), + ); + } + + try { + return await deferred.promise; } finally { disposables.forEach((d) => d.dispose()); } @@ -158,7 +220,6 @@ export class MultiStepInput implements IMultiStepInput { validate, password, buttons, - shouldResume }: P): Promise> { const disposables: Disposable[] = []; try { @@ -167,7 +228,7 @@ export class MultiStepInput implements IMultiStepInput { input.title = title; input.step = step; input.totalSteps = totalSteps; - input.password = password ? true : false; + input.password = !!password; input.value = value || ''; input.prompt = prompt; input.ignoreFocusOut = true; @@ -178,7 +239,8 @@ export class MultiStepInput implements IMultiStepInput { if (item === QuickInputButtons.Back) { reject(InputFlowAction.back); } else { - resolve(item); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(item as any); } }), input.onDidAccept(async () => { @@ -200,12 +262,8 @@ export class MultiStepInput implements IMultiStepInput { } }), input.onDidHide(() => { - (async () => { - reject( - shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel - ); - })().catch(reject); - }) + resolve(undefined); + }), ); if (this.current) { this.current.dispose(); @@ -232,6 +290,9 @@ export class MultiStepInput implements IMultiStepInput { if (err === InputFlowAction.back) { this.steps.pop(); step = this.steps.pop(); + if (step === undefined) { + throw err; + } } else if (err === InputFlowAction.resume) { step = this.steps.pop(); } else if (err === InputFlowAction.cancel) { @@ -253,6 +314,7 @@ export interface IMultiStepInputFactory { @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 index 770e15e0539e..a1a49ba3c427 100644 --- a/src/client/common/utils/platform.ts +++ b/src/client/common/utils/platform.ts @@ -3,16 +3,18 @@ 'use strict'; +import { EnvironmentVariables } from '../variables/types'; + export enum Architecture { Unknown = 1, x86 = 2, - x64 = 3 + x64 = 3, } export enum OSType { Unknown = 'Unknown', Windows = 'Windows', OSX = 'OSX', - Linux = 'Linux' + Linux = 'Linux', } // Return the OS type for the given platform string. @@ -27,3 +29,53 @@ export function getOSType(platform: string = process.platform): OSType { return OSType.Unknown; } } + +const architectures: Record = { + x86: Architecture.x86, // 32-bit + x64: Architecture.x64, // 64-bit + '': Architecture.Unknown, +}; + +/** + * Identify the host's native architecture/bitness. + */ +export function getArchitecture(): Architecture { + const fromProc = architectures[process.arch]; + if (fromProc !== undefined) { + return fromProc; + } + + const arch = require('arch'); + return architectures[arch()] || Architecture.Unknown; +} + +/** + * Look up the requested env var value (or undefined` if not set). + */ +export function getEnvironmentVariable(key: string): string | undefined { + return ((process.env as any) as EnvironmentVariables)[key]; +} + +/** + * Get the current user's home directory. + * + * The lookup is limited to environment variables. + */ +export function getUserHomeDir(): string | undefined { + if (getOSType() === OSType.Windows) { + return getEnvironmentVariable('USERPROFILE'); + } + return getEnvironmentVariable('HOME') || getEnvironmentVariable('HOMEPATH'); +} + +export function isWindows(): boolean { + return getOSType() === OSType.Windows; +} + +export function getPathEnvVariable(): string[] { + const value = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path'); + if (value) { + return value.split(isWindows() ? ';' : ':'); + } + return []; +} diff --git a/src/client/common/utils/random.ts b/src/client/common/utils/random.ts index 872766274ff2..a766df771116 100644 --- a/src/client/common/utils/random.ts +++ b/src/client/common/utils/random.ts @@ -17,7 +17,7 @@ function getRandom(): number { return num / maxValue; } -export function getRandomBetween(min: number = 0, max: number = 10): number { +function getRandomBetween(min: number = 0, max: number = 10): number { const randomVal: number = getRandom(); return min + randomVal * (max - min); } diff --git a/src/client/common/utils/regexp.ts b/src/client/common/utils/regexp.ts index 2d20b73e7f28..d05d7fc60204 100644 --- a/src/client/common/utils/regexp.ts +++ b/src/client/common/utils/regexp.ts @@ -15,8 +15,13 @@ * 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): RegExp { +export function verboseRegExp(pattern: string, flags?: string): RegExp { + pattern = pattern.replace(/(^| {2})# .*$/gm, ''); pattern = pattern.replace(/\s+?/g, ''); - return RegExp(pattern); + return RegExp(pattern, flags); } diff --git a/src/client/common/utils/resourceLifecycle.ts b/src/client/common/utils/resourceLifecycle.ts new file mode 100644 index 000000000000..b5d1a9a1c83a --- /dev/null +++ b/src/client/common/utils/resourceLifecycle.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { traceWarn } from '../../logging'; +import { IDisposable } from '../types'; +import { Iterable } from './iterable'; + +interface IDisposables extends IDisposable { + push(...disposable: IDisposable[]): void; +} + +export const EmptyDisposable = { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + dispose: () => { + /** */ + }, +}; + +/** + * Disposes of the value(s) passed in. + */ +export function dispose(disposable: T): T; +export function dispose(disposable: T | undefined): T | undefined; +export function dispose = Iterable>(disposables: A): A; +export function dispose(disposables: Array): Array; +export function dispose(disposables: ReadonlyArray): ReadonlyArray; +// eslint-disable-next-line @typescript-eslint/no-explicit-any, consistent-return +export function dispose(arg: T | Iterable | undefined): any { + if (Iterable.is(arg)) { + for (const d of arg) { + if (d) { + try { + d.dispose(); + } catch (e) { + traceWarn(`dispose() failed for ${d}`, e); + } + } + } + + return Array.isArray(arg) ? [] : arg; + } + if (arg) { + arg.dispose(); + return arg; + } +} + +/** + * Safely dispose each of the disposables. + */ +export async function disposeAll(disposables: IDisposable[]): Promise { + await Promise.all( + disposables.map(async (d) => { + try { + return Promise.resolve(d.dispose()); + } catch (err) { + // do nothing + } + return Promise.resolve(); + }), + ); +} + +/** + * A list of disposables. + */ +export class Disposables implements IDisposables { + private disposables: IDisposable[] = []; + + constructor(...disposables: IDisposable[]) { + this.disposables.push(...disposables); + } + + public push(...disposables: IDisposable[]): void { + this.disposables.push(...disposables); + } + + public async dispose(): Promise { + const { disposables } = this; + this.disposables = []; + await disposeAll(disposables); + } +} + +/** + * Manages a collection of disposable values. + * + * This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an + * `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a + * store that has already been disposed of. + */ +export class DisposableStore implements IDisposable { + static DISABLE_DISPOSED_WARNING = false; + + private readonly _toDispose = new Set(); + + private _isDisposed = false; + + constructor(...disposables: IDisposable[]) { + disposables.forEach((disposable) => this.add(disposable)); + } + + /** + * Dispose of all registered disposables and mark this object as disposed. + * + * Any future disposables added to this object will be disposed of on `add`. + */ + public dispose(): void { + if (this._isDisposed) { + return; + } + + this._isDisposed = true; + this.clear(); + } + + /** + * @return `true` if this object has been disposed of. + */ + public get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Dispose of all registered disposables but do not mark this object as disposed. + */ + public clear(): void { + if (this._toDispose.size === 0) { + return; + } + + try { + dispose(this._toDispose); + } finally { + this._toDispose.clear(); + } + } + + /** + * Add a new {@link IDisposable disposable} to the collection. + */ + public add(o: T): T { + if (!o) { + return o; + } + if (((o as unknown) as DisposableStore) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + + if (this._isDisposed) { + if (!DisposableStore.DISABLE_DISPOSED_WARNING) { + traceWarn( + new Error( + 'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!', + ).stack, + ); + } + } else { + this._toDispose.add(o); + } + + return o; + } +} + +/** + * Abstract class for a {@link IDisposable disposable} object. + * + * Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of. + */ +export abstract class DisposableBase implements IDisposable { + protected readonly _store = new DisposableStore(); + + private _isDisposed = false; + + public get isDisposed(): boolean { + return this._isDisposed; + } + + constructor(...disposables: IDisposable[]) { + disposables.forEach((disposable) => this._store.add(disposable)); + } + + public dispose(): void { + this._store.dispose(); + this._isDisposed = true; + } + + /** + * Adds `o` to the collection of disposables managed by this object. + */ + public _register(o: T): T { + if (((o as unknown) as DisposableBase) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + return this._store.add(o); + } +} diff --git a/src/client/common/utils/runAfterActivation.ts b/src/client/common/utils/runAfterActivation.ts new file mode 100644 index 000000000000..9a5297ea00f7 --- /dev/null +++ b/src/client/common/utils/runAfterActivation.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const itemsToRun: (() => void)[] = []; +let activationCompleted = false; + +/** + * Add items to be run after extension activation. This will add item + * to the end of the list. This function will immediately run the item + * if extension is already activated. + */ +export function addItemsToRunAfterActivation(run: () => void): void { + if (activationCompleted) { + run(); + } else { + itemsToRun.push(run); + } +} + +/** + * This should be called after extension activation is complete. + */ +export function runAfterActivation(): void { + activationCompleted = true; + while (itemsToRun.length > 0) { + const run = itemsToRun.shift(); + if (run) { + run(); + } + } +} diff --git a/src/client/common/utils/serializers.ts b/src/client/common/utils/serializers.ts deleted file mode 100644 index 7626c0780264..000000000000 --- a/src/client/common/utils/serializers.ts +++ /dev/null @@ -1,75 +0,0 @@ -// 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 index c78c763f7d2c..9c9a73d8279e 100644 --- a/src/client/common/utils/stopWatch.ts +++ b/src/client/common/utils/stopWatch.ts @@ -3,7 +3,7 @@ 'use strict'; -export class StopWatch { +export class StopWatch implements IStopWatch { private started = new Date().getTime(); public get elapsedTime() { return new Date().getTime() - this.started; @@ -12,3 +12,7 @@ export class StopWatch { this.started = new Date().getTime(); } } + +export interface IStopWatch { + elapsedTime: number; +} diff --git a/src/client/common/utils/sysTypes.ts b/src/client/common/utils/sysTypes.ts index ce0bce0af963..e56f12e34fff 100644 --- a/src/client/common/utils/sysTypes.ts +++ b/src/client/common/utils/sysTypes.ts @@ -5,14 +5,12 @@ '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' + function: 'function', }; /** @@ -41,13 +39,6 @@ export function isString(str: any): str is string { 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** @@ -74,57 +65,3 @@ export function isNumber(obj: any): obj is number { 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 index 59359966db47..ee61cae5bb1e 100644 --- a/src/client/common/utils/text.ts +++ b/src/client/common/utils/text.ts @@ -6,8 +6,8 @@ import { Position, Range, TextDocument } from 'vscode'; import { isNumber } from './sysTypes'; -export function getWindowsLineEndingCount(document: TextDocument, offset: Number) { - //const eolPattern = new RegExp('\r\n', 'g'); +export function getWindowsLineEndingCount(document: TextDocument, offset: number): number { + // const eolPattern = new RegExp('\r\n', 'g'); const eolPattern = /\r\n/g; const readBlock = 1024; let count = 0; @@ -111,3 +111,141 @@ export function parsePosition(raw: string | number): Position { } return new Position(line, col); } + +/** + * 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.length === 0) { + return []; + } + + 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; +} + +/** + * Extract a tree based on the given text. + * + * The tree is derived from the indent level of each line. The caller + * is responsible for applying any meaning to the text of each node + * in the tree. + * + * Blank lines and comments (with a leading `#`) are ignored. Also, + * the full text is automatically dedented until at least one line + * has no indent (i.e. is treated as a root). + * + * @returns - the list of nodes in the tree (pairs of text & parent index) + * (note that the parent index of roots is `-1`) + * + * Example: + * + * parseTree(` + * # This comment and the following blank line are ignored. + * + * this is a root + * the first branch + * a sub-branch # This comment is ignored. + * this is the first leaf node! + * another leaf node... + * middle + * + * the second main branch + * # indents do not have to be consistent across the full text. + * # ...and the indent of comments is not relevant. + * node 1 + * node 2 + * + * the last leaf node! + * + * another root + * nothing to see here! + * + * # this comment is ignored + * `.trim()) + * + * would produce the following: + * + * [ + * ['this is a root', -1], + * ['the first branch', 0], + * ['a sub-branch', 1], + * ['this is the first leaf node!', 2], + * ['another leaf node...', 1], + * ['middle', 1], + * ['the second main branch', 0], + * ['node 1', 6], + * ['node 2', 6], + * ['the last leaf node!', 0], + * ['another root', -1], + * ['nothing to see here!', 10], + * ] + */ +export function parseTree(text: string): [string, number][] { + const parsed: [string, number][] = []; + const parents: [string, number][] = []; + + const lines = getDedentedLines(text) + .map((l) => l.split(' #')[0].split(' //')[0].trimEnd()) + .filter((l) => l.trim() !== ''); + lines.forEach((line) => { + const indent = getIndent(line); + const entry = line.trim(); + + let parentIndex: number; + if (indent === '') { + parentIndex = -1; + parents.push([indent, parsed.length]); + } else if (parsed.length === 0) { + throw Error(`expected non-indented line, got ${line}`); + } else { + let parentIndent: string; + [parentIndent, parentIndex] = parents[parents.length - 1]; + while (indent.length <= parentIndent.length) { + parents.pop(); + [parentIndent, parentIndex] = parents[parents.length - 1]; + } + if (parentIndent.length < indent.length) { + parents.push([indent, parsed.length]); + } + } + parsed.push([entry, parentIndex!]); + }); + + return parsed; +} diff --git a/src/client/common/utils/version.ts b/src/client/common/utils/version.ts index cad0bea9ad92..b3d9ed3d2f46 100644 --- a/src/client/common/utils/version.ts +++ b/src/client/common/utils/version.ts @@ -4,12 +4,402 @@ 'use strict'; import * as semver from 'semver'; +import { verboseRegExp } from './regexp'; -export function parseVersion(raw: string): semver.SemVer { +// 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 (Number.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 (Number.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, +}; +Object.freeze(EMPTY_VERSION); + +function copyStrict(info: T): RawBasicVersionInfo { + const copied: RawBasicVersionInfo = { + major: info.major, + minor: info.minor, + micro: info.micro, + }; + + const { unnormalized } = (info as unknown) as RawBasicVersionInfo; + if (unnormalized !== undefined) { + copied.unnormalized = { + major: unnormalized.major, + minor: unnormalized.minor, + micro: unnormalized.micro, + }; + } + + return copied; +} + +/** + * Make a copy and set all the properties properly. + * + * Only the "basic" version info will be set (and normalized). + * The caller is responsible for any other properties beyond that. + */ +function normalizeBasicVersionInfo(info: T | undefined): T { + if (!info) { + return EMPTY_VERSION as T; + } + const norm = copyStrict(info); + // Do not normalize if it has already been normalized. + if (norm.unnormalized === undefined) { + norm.unnormalized = {}; + [norm.major, norm.unnormalized.major] = normalizeVersionPart(norm.major); + [norm.minor, norm.unnormalized.minor] = normalizeVersionPart(norm.minor); + [norm.micro, norm.unnormalized.micro] = normalizeVersionPart(norm.micro); + } + return norm as T; +} + +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. + */ +function validateBasicVersionInfo(info: T): void { + 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 ''; + } + if (info.minor < 0) { + return `${info.major}`; + } + 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; +} + +/** + * Decide if two versions are the same or if one is "less". + * + * Note that a less-complete object that otherwise matches + * is considered "less". + * + * Additional checks for an otherwise "identical" version may be made + * through `compareExtra()`. + * + * @returns - the customary comparison indicator (e.g. -1 means left is "more") + * @returns - a string that indicates the property where they differ (if any) + */ +export function compareVersions( + // the versions to compare: + left: T, + right: V, + compareExtra?: (v1: T, v2: V) => [number, string], +): [number, string] { + if (left.major < right.major) { + return [1, 'major']; + } + if (left.major > right.major) { + return [-1, 'major']; + } + if (left.major === -1) { + // Don't bother checking minor or micro. + return [0, '']; + } + + if (left.minor < right.minor) { + return [1, 'minor']; + } + if (left.minor > right.minor) { + return [-1, 'minor']; + } + if (left.minor === -1) { + // Don't bother checking micro. + return [0, '']; + } + + if (left.micro < right.micro) { + return [1, 'micro']; + } + if (left.micro > right.micro) { + return [-1, 'micro']; + } + + if (compareExtra !== undefined) { + return compareExtra(left, right); + } + + return [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 norm = normalizeBasicVersionInfo(info); + norm.raw = info.raw; + if (!norm.raw) { + norm.raw = ''; + } + // Any string value of "raw" is considered normalized. + 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): void { + 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; +} + +/** + * Checks if major, minor, and micro match. + * + * Additional checks may be made through `compareExtra()`. + */ +export function areIdenticalVersion( + // the versions to compare: + left: T, + right: V, + compareExtra?: (v1: T, v2: V) => [number, string], +): boolean { + const [result] = compareVersions(left, right, compareExtra); + return result === 0; +} + +/** + * Checks if the versions are identical or one is more complete than other (and otherwise the same). + * + * At the least the major version must be set (non-negative). + */ +export function areSimilarVersions( + // the versions to compare: + left: T, + right: V, + compareExtra?: (v1: T, v2: V) => [number, string], +): boolean { + const [result, prop] = compareVersions(left, right, compareExtra); + if (result === 0) { + return true; + } + + if (prop === 'major') { + // An empty version is never similar (except to another empty version). + return false; + } + + if (result < 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((right as unknown) as any)[prop] === -1; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((left as unknown) as any)[prop] === -1; +} + +// semver + +export function parseSemVerSafe(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'); } diff --git a/src/client/common/utils/workerPool.ts b/src/client/common/utils/workerPool.ts new file mode 100644 index 000000000000..a241c416f3bd --- /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 '../../logging'; +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 as Error); + } + } 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 createRunningWorkerPool( + workerFunc: WorkFunc, + numWorkers?: number, + name?: string, +): WorkerPool { + 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 index 63f7896d5423..9f0abd9b0ee7 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -1,13 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { pathExistsSync, readFileSync } from '../platform/fs-paths'; import { inject, injectable } from 'inversify'; import * as path from 'path'; +import { traceError } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IFileSystem } from '../platform/types'; import { IPathUtils } from '../types'; import { EnvironmentVariables, IEnvironmentVariablesService } from './types'; +import { normCase } from '../platform/fs-paths'; @injectable() export class EnvironmentVariablesService implements IEnvironmentVariablesService { @@ -15,32 +18,64 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService constructor( // We only use a small portion of either of these interfaces. @inject(IPathUtils) private readonly pathUtils: IPathUtils, - @inject(IFileSystem) private readonly fs: IFileSystem + @inject(IFileSystem) private readonly fs: IFileSystem, ) {} public async parseFile( filePath?: string, - baseVars?: EnvironmentVariables + baseVars?: EnvironmentVariables, ): Promise { - if (!filePath || !(await this.fs.fileExists(filePath))) { + if (!filePath || !(await this.fs.pathExists(filePath))) { return; } - return parseEnvFile(await this.fs.readFile(filePath), baseVars); + const contents = await this.fs.readFile(filePath).catch((ex) => { + traceError('Custom .env is likely not pointing to a valid file', ex); + return undefined; + }); + if (!contents) { + return; + } + return parseEnvFile(contents, baseVars); } - public mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables) { + public parseFileSync(filePath?: string, baseVars?: EnvironmentVariables): EnvironmentVariables | undefined { + if (!filePath || !pathExistsSync(filePath)) { + return; + } + let contents: string | undefined; + try { + contents = readFileSync(filePath, { encoding: 'utf8' }); + } catch (ex) { + traceError('Custom .env is likely not pointing to a valid file', ex); + } + if (!contents) { + return; + } + return parseEnvFile(contents, baseVars); + } + + public mergeVariables( + source: EnvironmentVariables, + target: EnvironmentVariables, + options?: { overwrite?: boolean; mergeAll?: boolean }, + ) { if (!target) { return; } + const reference = target; + target = normCaseKeys(target); + source = normCaseKeys(source); const settingsNotToMerge = ['PYTHONPATH', this.pathVariable]; Object.keys(source).forEach((setting) => { - if (settingsNotToMerge.indexOf(setting) >= 0) { + if (!options?.mergeAll && settingsNotToMerge.indexOf(setting) >= 0) { return; } - if (target[setting] === undefined) { + if (target[setting] === undefined || options?.overwrite) { target[setting] = source[setting]; } }); + restoreKeys(target); + matchTarget(reference, target); } public appendPythonPath(vars: EnvironmentVariables, ...pythonPaths: string[]) { @@ -51,18 +86,24 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService return this.appendPaths(vars, this.pathVariable, ...paths); } - private get pathVariable(): 'Path' | 'PATH' { + private get pathVariable(): string { if (!this._pathVariable) { this._pathVariable = this.pathUtils.getPathVariableName(); } - return this._pathVariable!; + return normCase(this._pathVariable)!; } - private appendPaths( - vars: EnvironmentVariables, - variableName: 'PATH' | 'Path' | 'PYTHONPATH', - ...pathsToAppend: string[] - ) { + private appendPaths(vars: EnvironmentVariables, variableName: string, ...pathsToAppend: string[]) { + const reference = vars; + vars = normCaseKeys(vars); + variableName = normCase(variableName); + vars = this._appendPaths(vars, variableName, ...pathsToAppend); + restoreKeys(vars); + matchTarget(reference, vars); + return vars; + } + + private _appendPaths(vars: EnvironmentVariables, variableName: string, ...pathsToAppend: string[]) { const valueToAppend = pathsToAppend .filter((item) => typeof item === 'string' && item.trim().length > 0) .map((item) => item.trim()) @@ -102,7 +143,7 @@ function parseEnvLine(line: string): [string, string] { // 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*$/); + const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/); if (!match) { return ['', '']; } @@ -130,7 +171,7 @@ function substituteEnvVars( value: string, localVars: EnvironmentVariables, globalVars: EnvironmentVariables, - missing = '' + missing = '', ): string { // Substitution here is inspired a little by dotenv-expand: // https://github.com/motdotla/dotenv-expand/blob/master/lib/main.js @@ -154,3 +195,40 @@ function substituteEnvVars( return value.replace(/\\\$/g, '$'); } + +export function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const normalizedEnv: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + const normalizedKey = normCase(key); + normalizedEnv[normalizedKey] = env[key]; + }); + return normalizedEnv; +} + +export function restoreKeys(env: EnvironmentVariables) { + const processEnvKeys = Object.keys(process.env); + processEnvKeys.forEach((processEnvKey) => { + const originalKey = normCase(processEnvKey); + if (originalKey !== processEnvKey && env[originalKey] !== undefined) { + env[processEnvKey] = env[originalKey]; + delete env[originalKey]; + } + }); +} + +export function matchTarget(reference: EnvironmentVariables, target: EnvironmentVariables): void { + Object.keys(reference).forEach((key) => { + if (target.hasOwnProperty(key)) { + reference[key] = target[key]; + } else { + delete reference[key]; + } + }); + + // Add any new keys from target to reference + Object.keys(target).forEach((key) => { + if (!reference.hasOwnProperty(key)) { + reference[key] = target[key]; + } + }); +} diff --git a/src/client/common/variables/environmentVariablesProvider.ts b/src/client/common/variables/environmentVariablesProvider.ts index fd083b10e88f..14573d2204aa 100644 --- a/src/client/common/variables/environmentVariablesProvider.ts +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -1,34 +1,39 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, FileSystemWatcher, Uri } from 'vscode'; -import { IServiceContainer } from '../../ioc/types'; +import { traceError, traceVerbose } from '../../logging'; import { sendFileCreationTelemetry } from '../../telemetry/envFileTelemetry'; import { IWorkspaceService } from '../application/types'; -import { traceVerbose } from '../logger'; +import { PythonSettings } from '../configSettings'; import { IPlatformService } from '../platform/types'; -import { IConfigurationService, ICurrentProcess, IDisposableRegistry } from '../types'; -import { InMemoryInterpreterSpecificCache } from '../utils/cacheUtils'; -import { clearCachedResourceSpecificIngterpreterData } from '../utils/decorators'; +import { ICurrentProcess, IDisposableRegistry } from '../types'; +import { InMemoryCache } from '../utils/cacheUtils'; +import { SystemVariables } from './systemVariables'; 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; + + private readonly envVarCaches = new Map>(); + 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 + private cacheDuration: number = CACHE_DURATION, ) { disposableRegistry.push(this); this.changeEventEmitter = new EventEmitter(); @@ -40,7 +45,7 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid return this.changeEventEmitter.event; } - public dispose() { + public dispose(): void { this.changeEventEmitter.dispose(); this.fileWatchers.forEach((watcher) => { if (watcher) { @@ -50,23 +55,56 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } 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 cached = this.getCachedEnvironmentVariables(resource); + if (cached) { + return cached; + } + const vars = await this._getEnvironmentVariables(resource); + this.setCachedEnvironmentVariables(resource, vars); + traceVerbose('Dump environment variables', JSON.stringify(vars, null, 4)); + return vars; + } + + public getEnvironmentVariablesSync(resource?: Uri): EnvironmentVariables { + const cached = this.getCachedEnvironmentVariables(resource); + if (cached) { + return cached; } - const promise = this._getEnvironmentVariables(resource); - promise.then((result) => (cacheStore.data = result)).ignoreErrors(); - return promise; + const vars = this._getEnvironmentVariablesSync(resource); + this.setCachedEnvironmentVariables(resource, vars); + return vars; } + + private getCachedEnvironmentVariables(resource?: Uri): EnvironmentVariables | undefined { + const cacheKey = this.getWorkspaceFolderUri(resource)?.fsPath ?? ''; + const cache = this.envVarCaches.get(cacheKey); + if (cache) { + const cachedData = cache.data; + if (cachedData) { + return { ...cachedData }; + } + } + return undefined; + } + + private setCachedEnvironmentVariables(resource: Uri | undefined, vars: EnvironmentVariables): void { + const cacheKey = this.getWorkspaceFolderUri(resource)?.fsPath ?? ''; + const cache = new InMemoryCache(this.cacheDuration); + this.envVarCaches.set(cacheKey, cache); + cache.data = { ...vars }; + } + public async _getEnvironmentVariables(resource?: Uri): Promise { - let mergedVars = await this.getCustomEnvironmentVariables(resource); + const customVars = await this.getCustomEnvironmentVariables(resource); + return this.getMergedEnvironmentVariables(customVars); + } + + public _getEnvironmentVariablesSync(resource?: Uri): EnvironmentVariables { + const customVars = this.getCustomEnvironmentVariablesSync(resource); + return this.getMergedEnvironmentVariables(customVars); + } + + private getMergedEnvironmentVariables(mergedVars?: EnvironmentVariables): EnvironmentVariables { if (!mergedVars) { mergedVars = {}; } @@ -81,14 +119,34 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } return mergedVars; } + public async getCustomEnvironmentVariables(resource?: Uri): Promise { - const settings = this.configurationService.getSettings(resource); + return this.envVarsService.parseFile(this.getEnvFile(resource), this.process.env); + } + + private getCustomEnvironmentVariablesSync(resource?: Uri): EnvironmentVariables | undefined { + return this.envVarsService.parseFileSync(this.getEnvFile(resource), this.process.env); + } + + private getEnvFile(resource?: Uri): string { + const systemVariables: SystemVariables = new SystemVariables( + undefined, + PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri?.fsPath, + this.workspaceService, + ); const workspaceFolderUri = this.getWorkspaceFolderUri(resource); + const envFileSetting = this.workspaceService.getConfiguration('python', resource).get('envFile'); + const envFile = systemVariables.resolveAny(envFileSetting); + if (envFile === undefined) { + traceError('Unable to read `python.envFile` setting for resource', JSON.stringify(resource)); + return workspaceFolderUri?.fsPath ? path.join(workspaceFolderUri?.fsPath, '.env') : ''; + } this.trackedWorkspaceFolders.add(workspaceFolderUri ? workspaceFolderUri.fsPath : ''); - this.createFileWatcher(settings.envFile, workspaceFolderUri); - return this.envVarsService.parseFile(settings.envFile, this.process.env); + this.createFileWatcher(envFile, workspaceFolderUri); + return envFile; } - public configurationChanged(e: ConfigurationChangeEvent) { + + public configurationChanged(e: ConfigurationChangeEvent): void { this.trackedWorkspaceFolders.forEach((item) => { const uri = item && item.length > 0 ? Uri.file(item) : undefined; if (e.affectsConfiguration('python.envFile', uri)) { @@ -96,7 +154,8 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } }); } - public createFileWatcher(envFile: string, workspaceFolderUri?: Uri) { + + public createFileWatcher(envFile: string, workspaceFolderUri?: Uri): void { if (this.fileWatchers.has(envFile)) { return; } @@ -108,9 +167,10 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid this.disposables.push(envFileWatcher.onDidDelete(() => this.onEnvironmentFileChanged(workspaceFolderUri))); } } + private getWorkspaceFolderUri(resource?: Uri): Uri | undefined { if (!resource) { - return; + return undefined; } const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource!); return workspaceFolder ? workspaceFolder.uri : undefined; @@ -122,16 +182,8 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } private onEnvironmentFileChanged(workspaceFolderUri?: Uri) { - clearCachedResourceSpecificIngterpreterData( - 'getEnvironmentVariables', - workspaceFolderUri, - this.serviceContainer - ); - clearCachedResourceSpecificIngterpreterData( - 'CustomEnvironmentVariables', - workspaceFolderUri, - this.serviceContainer - ); + // An environment file changing can affect multiple workspaces; clear everything and reparse later. + this.envVarCaches.clear(); this.changeEventEmitter.fire(workspaceFolderUri); } } diff --git a/src/client/common/variables/serviceRegistry.ts b/src/client/common/variables/serviceRegistry.ts index 3a98c3cbfea4..db4f620ab6a7 100644 --- a/src/client/common/variables/serviceRegistry.ts +++ b/src/client/common/variables/serviceRegistry.ts @@ -9,10 +9,10 @@ import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from './t export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton( IEnvironmentVariablesService, - EnvironmentVariablesService + EnvironmentVariablesService, ); serviceManager.addSingleton( IEnvironmentVariablesProvider, - EnvironmentVariablesProvider + EnvironmentVariablesProvider, ); } diff --git a/src/client/common/variables/sysTypes.ts b/src/client/common/variables/sysTypes.ts deleted file mode 100644 index 10bd2b776b17..000000000000 --- a/src/client/common/variables/sysTypes.ts +++ /dev/null @@ -1,39 +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'; - -// 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 index 74b307dafb78..05e5d9d6f584 100644 --- a/src/client/common/variables/systemVariables.ts +++ b/src/client/common/variables/systemVariables.ts @@ -7,18 +7,17 @@ import * as Path from 'path'; import { Range, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../application/types'; +import { WorkspaceService } from '../application/workspace'; 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); @@ -32,7 +31,7 @@ abstract class AbstractSystemVariables implements ISystemVariables { } public resolveAny(value: T): T; - // tslint:disable-next-line:no-any + public resolveAny(value: any): any { if (Types.isString(value)) { return this.__resolveString(value); @@ -48,7 +47,6 @@ abstract class AbstractSystemVariables implements ISystemVariables { 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; @@ -59,24 +57,24 @@ abstract class AbstractSystemVariables implements ISystemVariables { } private __resolveLiteral( - values: IStringDictionary | string[]> + 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; @@ -87,7 +85,7 @@ abstract class AbstractSystemVariables implements ISystemVariables { } private __resolveAnyArray(value: T[]): T[]; - // tslint:disable-next-line:no-any + private __resolveAnyArray(value: any[]): any[] { return value.map((s) => this.resolveAny(s)); } @@ -105,7 +103,7 @@ export class SystemVariables extends AbstractSystemVariables { file: Uri | undefined, rootFolder: string | undefined, workspace?: IWorkspaceService, - documentManager?: IDocumentManager + documentManager?: IDocumentManager, ) { super(); const workspaceFolder = workspace && file ? workspace.getWorkspaceFolder(file) : undefined; @@ -117,8 +115,8 @@ export class SystemVariables extends AbstractSystemVariables { this._selectedText = documentManager.activeTextEditor.document.getText( new Range( documentManager.activeTextEditor.selection.start, - documentManager.activeTextEditor.selection.end - ) + documentManager.activeTextEditor.selection.end, + ), ); } this._execPath = process.execPath; @@ -128,6 +126,18 @@ export class SystemVariables extends AbstractSystemVariables { string | undefined >)[`env.${key}`] = process.env[key]; }); + workspace = workspace ?? new WorkspaceService(); + try { + workspace.workspaceFolders?.forEach((folder) => { + const basename = Path.basename(folder.uri.fsPath); + ((this as any) as Record)[`workspaceFolder:${basename}`] = + folder.uri.fsPath; + ((this as any) as Record)[`workspaceFolder:${folder.name}`] = + folder.uri.fsPath; + }); + } catch { + // This try...catch block is here to support pre-existing tests, ignore error. + } } public get cwd(): string { diff --git a/src/client/common/variables/types.ts b/src/client/common/variables/types.ts index c1b844451021..252a0d48038f 100644 --- a/src/client/common/variables/types.ts +++ b/src/client/common/variables/types.ts @@ -9,7 +9,12 @@ export const IEnvironmentVariablesService = Symbol('IEnvironmentVariablesService export interface IEnvironmentVariablesService { parseFile(filePath?: string, baseVars?: EnvironmentVariables): Promise; - mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables): void; + parseFileSync(filePath?: string, baseVars?: EnvironmentVariables): EnvironmentVariables | undefined; + mergeVariables( + source: EnvironmentVariables, + target: EnvironmentVariables, + options?: { overwrite?: boolean; mergeAll?: boolean }, + ): void; appendPythonPath(vars: EnvironmentVariables, ...pythonPaths: string[]): void; appendPath(vars: EnvironmentVariables, ...paths: string[]): void; } @@ -29,7 +34,7 @@ export interface ISystemVariables { resolve(value: IStringDictionary): IStringDictionary; resolve(value: IStringDictionary>): IStringDictionary>; resolveAny(value: T): T; - // tslint:disable-next-line:no-any + [key: string]: any; } @@ -38,5 +43,5 @@ export const IEnvironmentVariablesProvider = Symbol('IEnvironmentVariablesProvid export interface IEnvironmentVariablesProvider { onDidEnvironmentVariablesChange: Event; getEnvironmentVariables(resource?: Uri): Promise; - getCustomEnvironmentVariables(resource?: Uri): Promise; + getEnvironmentVariablesSync(resource?: Uri): EnvironmentVariables; } diff --git a/src/client/common/vscodeApis/browserApis.ts b/src/client/common/vscodeApis/browserApis.ts new file mode 100644 index 000000000000..ccf51bd07ec8 --- /dev/null +++ b/src/client/common/vscodeApis/browserApis.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { env, Uri } from 'vscode'; + +export function launch(url: string): void { + env.openExternal(Uri.parse(url)); +} diff --git a/src/client/common/vscodeApis/commandApis.ts b/src/client/common/vscodeApis/commandApis.ts new file mode 100644 index 000000000000..908cb761c538 --- /dev/null +++ b/src/client/common/vscodeApis/commandApis.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { commands, Disposable } from 'vscode'; + +/** + * Wrapper for vscode.commands.executeCommand to make it easier to mock in tests + */ +export function executeCommand(command: string, ...rest: any[]): Thenable { + return commands.executeCommand(command, ...rest); +} + +/** + * Wrapper for vscode.commands.registerCommand to make it easier to mock in tests + */ +export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { + return commands.registerCommand(command, callback, thisArg); +} diff --git a/src/client/common/vscodeApis/extensionsApi.ts b/src/client/common/vscodeApis/extensionsApi.ts new file mode 100644 index 000000000000..f099d6f636b0 --- /dev/null +++ b/src/client/common/vscodeApis/extensionsApi.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as fs from '../platform/fs-paths'; +import { PVSC_EXTENSION_ID } from '../constants'; + +export function getExtension(extensionId: string): vscode.Extension | undefined { + return vscode.extensions.getExtension(extensionId); +} + +export function isExtensionEnabled(extensionId: string): boolean { + return vscode.extensions.getExtension(extensionId) !== undefined; +} + +export function isExtensionDisabled(extensionId: string): boolean { + // We need an enabled extension to find the extensions dir. + const pythonExt = getExtension(PVSC_EXTENSION_ID); + if (pythonExt) { + let found = false; + fs.readdirSync(path.dirname(pythonExt.extensionPath), { withFileTypes: false }).forEach((s) => { + if (s.toString().startsWith(extensionId)) { + found = true; + } + }); + return found; + } + return false; +} + +export function isInsider(): boolean { + return vscode.env.appName.includes('Insider'); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getExtensions(): readonly vscode.Extension[] { + return vscode.extensions.all; +} diff --git a/src/client/common/vscodeApis/languageApis.ts b/src/client/common/vscodeApis/languageApis.ts new file mode 100644 index 000000000000..87681507693d --- /dev/null +++ b/src/client/common/vscodeApis/languageApis.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { DiagnosticChangeEvent, DiagnosticCollection, Disposable, languages } from 'vscode'; + +export function createDiagnosticCollection(name: string): DiagnosticCollection { + return languages.createDiagnosticCollection(name); +} + +export function onDidChangeDiagnostics(handler: (e: DiagnosticChangeEvent) => void): Disposable { + return languages.onDidChangeDiagnostics(handler); +} diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts new file mode 100644 index 000000000000..90a06e7ed75a --- /dev/null +++ b/src/client/common/vscodeApis/windowApis.ts @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-classes-per-file */ + +import { + CancellationToken, + MessageItem, + MessageOptions, + Progress, + ProgressOptions, + QuickPick, + QuickInputButtons, + QuickPickItem, + QuickPickOptions, + TextEditor, + window, + Disposable, + QuickPickItemButtonEvent, + Uri, + TerminalShellExecutionStartEvent, + LogOutputChannel, + OutputChannel, + TerminalLinkProvider, + NotebookDocument, + NotebookEditor, + NotebookDocumentShowOptions, + Terminal, +} from 'vscode'; +import { createDeferred, Deferred } from '../utils/async'; +import { Resource } from '../types'; +import { getWorkspaceFolders } from './workspaceApis'; + +export function showTextDocument(uri: Uri): Thenable { + return window.showTextDocument(uri); +} + +export function showNotebookDocument( + document: NotebookDocument, + options?: NotebookDocumentShowOptions, +): Thenable { + return window.showNotebookDocument(document, options); +} + +export function showQuickPick( + items: readonly T[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, +): Thenable { + return window.showQuickPick(items, options, token); +} + +export function createQuickPick(): QuickPick { + return window.createQuickPick(); +} + +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showErrorMessage(message: string, ...items: any[]): Thenable { + return window.showErrorMessage(message, ...items); +} + +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showWarningMessage(message: string, ...items: any[]): Thenable { + return window.showWarningMessage(message, ...items); +} + +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showInformationMessage(message: string, ...items: any[]): Thenable { + return window.showInformationMessage(message, ...items); +} + +export function withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, +): Thenable { + return window.withProgress(options, task); +} + +export function getActiveTextEditor(): TextEditor | undefined { + const { activeTextEditor } = window; + return activeTextEditor; +} + +export function onDidChangeActiveTextEditor(handler: (e: TextEditor | undefined) => void): Disposable { + return window.onDidChangeActiveTextEditor(handler); +} + +export function onDidStartTerminalShellExecution(handler: (e: TerminalShellExecutionStartEvent) => void): Disposable { + return window.onDidStartTerminalShellExecution(handler); +} + +export function onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); +} + +export enum MultiStepAction { + Back = 'Back', + Cancel = 'Cancel', + Continue = 'Continue', +} + +export async function showQuickPickWithBack( + items: readonly T[], + options?: QuickPickOptions, + token?: CancellationToken, + itemButtonHandler?: (e: QuickPickItemButtonEvent) => void, +): Promise { + const quickPick: QuickPick = window.createQuickPick(); + const disposables: Disposable[] = [quickPick]; + + quickPick.items = items; + quickPick.buttons = [QuickInputButtons.Back]; + quickPick.canSelectMany = options?.canPickMany ?? false; + quickPick.ignoreFocusOut = options?.ignoreFocusOut ?? false; + quickPick.matchOnDescription = options?.matchOnDescription ?? false; + quickPick.matchOnDetail = options?.matchOnDetail ?? false; + quickPick.placeholder = options?.placeHolder; + quickPick.title = options?.title; + + const deferred = createDeferred(); + + disposables.push( + quickPick, + quickPick.onDidTriggerButton((item) => { + if (item === QuickInputButtons.Back) { + deferred.reject(MultiStepAction.Back); + quickPick.hide(); + } + }), + quickPick.onDidAccept(() => { + if (!deferred.completed) { + if (quickPick.canSelectMany) { + deferred.resolve(quickPick.selectedItems.map((item) => item)); + } else { + deferred.resolve(quickPick.selectedItems[0]); + } + + quickPick.hide(); + } + }), + quickPick.onDidHide(() => { + if (!deferred.completed) { + deferred.resolve(undefined); + } + }), + quickPick.onDidTriggerItemButton((e) => { + if (itemButtonHandler) { + itemButtonHandler(e); + } + }), + ); + if (token) { + disposables.push( + token.onCancellationRequested(() => { + quickPick.hide(); + }), + ); + } + quickPick.show(); + + try { + return await deferred.promise; + } finally { + disposables.forEach((d) => d.dispose()); + } +} + +export class MultiStepNode { + constructor( + public previous: MultiStepNode | undefined, + public readonly current: (context?: MultiStepAction) => Promise, + public next: MultiStepNode | undefined, + ) {} + + public static async run(step: MultiStepNode, context?: MultiStepAction): Promise { + let nextStep: MultiStepNode | undefined = step; + let flowAction = await nextStep.current(context); + while (nextStep !== undefined) { + if (flowAction === MultiStepAction.Cancel) { + return flowAction; + } + if (flowAction === MultiStepAction.Back) { + nextStep = nextStep?.previous; + } + if (flowAction === MultiStepAction.Continue) { + nextStep = nextStep?.next; + } + + if (nextStep) { + flowAction = await nextStep?.current(flowAction); + } + } + + return flowAction; + } +} + +export function createStepBackEndNode(deferred?: Deferred): MultiStepNode { + return new MultiStepNode( + undefined, + async () => { + if (deferred) { + // This is to ensure we don't leave behind any pending promises. + deferred.reject(MultiStepAction.Back); + } + return Promise.resolve(MultiStepAction.Back); + }, + undefined, + ); +} + +export function createStepForwardEndNode(deferred?: Deferred, result?: T): MultiStepNode { + return new MultiStepNode( + undefined, + async () => { + if (deferred) { + // This is to ensure we don't leave behind any pending promises. + deferred.resolve(result); + } + return Promise.resolve(MultiStepAction.Back); + }, + undefined, + ); +} + +export function getActiveResource(): Resource { + const editor = window.activeTextEditor; + if (editor && !editor.document.isUntitled) { + return editor.document.uri; + } + const workspaces = getWorkspaceFolders(); + return Array.isArray(workspaces) && workspaces.length > 0 ? workspaces[0].uri : undefined; +} + +export function createOutputChannel(name: string, languageId?: string): OutputChannel { + return window.createOutputChannel(name, languageId); +} +export function createLogOutputChannel(name: string, options: { log: true }): LogOutputChannel { + return window.createOutputChannel(name, options); +} + +export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable { + return window.registerTerminalLinkProvider(provider); +} diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts new file mode 100644 index 000000000000..cd45f655702d --- /dev/null +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import { Resource } from '../types'; + +export function getWorkspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined { + return vscode.workspace.workspaceFolders; +} + +export function getWorkspaceFolder(uri: Resource): vscode.WorkspaceFolder | undefined { + return uri ? vscode.workspace.getWorkspaceFolder(uri) : undefined; +} + +export function getWorkspaceFolderPaths(): string[] { + return vscode.workspace.workspaceFolders?.map((w) => w.uri.fsPath) ?? []; +} + +export function getConfiguration( + section?: string, + scope?: vscode.ConfigurationScope | null, +): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration(section, scope); +} + +export function applyEdit(edit: vscode.WorkspaceEdit): Thenable { + return vscode.workspace.applyEdit(edit); +} + +export function findFiles( + include: vscode.GlobPattern, + exclude?: vscode.GlobPattern | null, + maxResults?: number, + token?: vscode.CancellationToken, +): Thenable { + return vscode.workspace.findFiles(include, exclude, maxResults, token); +} + +export function onDidCloseTextDocument(handler: (e: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidCloseTextDocument(handler); +} + +export function onDidSaveTextDocument(handler: (e: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(handler); +} + +export function getOpenTextDocuments(): readonly vscode.TextDocument[] { + return vscode.workspace.textDocuments; +} + +export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidOpenTextDocument(handler); +} + +export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => void): vscode.Disposable { + return vscode.workspace.onDidChangeTextDocument(handler); +} + +export function onDidChangeConfiguration(handler: (e: vscode.ConfigurationChangeEvent) => void): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(handler); +} + +export function onDidCloseNotebookDocument(handler: (e: vscode.NotebookDocument) => void): vscode.Disposable { + return vscode.workspace.onDidCloseNotebookDocument(handler); +} + +export function createFileSystemWatcher( + globPattern: vscode.GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, +): vscode.FileSystemWatcher { + return vscode.workspace.createFileSystemWatcher( + globPattern, + ignoreCreateEvents, + ignoreChangeEvents, + ignoreDeleteEvents, + ); +} + +export function onDidChangeWorkspaceFolders( + handler: (e: vscode.WorkspaceFoldersChangeEvent) => void, +): vscode.Disposable { + return vscode.workspace.onDidChangeWorkspaceFolders(handler); +} + +export function isVirtualWorkspace(): boolean { + const isVirtualWorkspace = + vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file'); + return !!isVirtualWorkspace; +} + +export function isTrusted(): boolean { + return vscode.workspace.isTrusted; +} + +export function onDidGrantWorkspaceTrust(handler: () => void): vscode.Disposable { + return vscode.workspace.onDidGrantWorkspaceTrust(handler); +} + +export function createDirectory(uri: vscode.Uri): Thenable { + return vscode.workspace.fs.createDirectory(uri); +} + +export function openNotebookDocument(uri: vscode.Uri): Thenable; +export function openNotebookDocument( + notebookType: string, + content?: vscode.NotebookData, +): Thenable; +export function openNotebookDocument(notebook: any, content?: vscode.NotebookData): Thenable { + return vscode.workspace.openNotebookDocument(notebook, content); +} + +export function copy(source: vscode.Uri, dest: vscode.Uri, options?: { overwrite?: boolean }): Thenable { + return vscode.workspace.fs.copy(source, dest, options); +} diff --git a/src/client/components.ts b/src/client/components.ts new file mode 100644 index 000000000000..f06f69eaac35 --- /dev/null +++ b/src/client/components.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry, IExtensionContext } from './common/types'; +import { IServiceContainer, IServiceManager } from './ioc/types'; + +/** + * The global extension state needed by components. + * + */ +export type ExtensionState = { + context: IExtensionContext; + disposables: IDisposableRegistry; + // For now we include the objects dealing with inversify (IOC) + // registration. These will be removed later. + legacyIOC: { + serviceManager: IServiceManager; + serviceContainer: IServiceContainer; + }; +}; + +/** + * The result of activating a component of the extension. + * + * Getting this value means the component has reached a state where it + * may be used by the rest of the extension. + * + * If the component started any non-critical activation-related + * operations during activation then the "fullyReady" property will only + * resolve once all those operations complete. + * + * The component may have also started long-running background helpers. + * Those are not exposed here. + */ +export type ActivationResult = { + fullyReady: Promise; +}; diff --git a/src/client/constants.ts b/src/client/constants.ts index 55e22eb0e4e3..48c5f55e5ce4 100644 --- a/src/client/constants.ts +++ b/src/client/constants.ts @@ -11,7 +11,4 @@ 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 deleted file mode 100644 index ccf319aa9b5c..000000000000 --- a/src/client/datascience/activation.ts +++ /dev/null @@ -1,69 +0,0 @@ -// 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 deleted file mode 100644 index cafafe68a639..000000000000 --- a/src/client/datascience/baseJupyterSession.ts +++ /dev/null @@ -1,508 +0,0 @@ -// 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 { PythonInterpreter } from '../pythonEnvironments/info'; -import { sendTelemetryEvent } from '../telemetry'; -import { Identifiers, Telemetry } from './constants'; -import { JupyterInvalidKernelError } from './jupyter/jupyterInvalidKernelError'; -import { JupyterWaitForIdleError } from './jupyter/jupyterWaitForIdleError'; -import { JupyterKernelPromiseFailedError } from './jupyter/kernels/jupyterKernelPromiseFailedError'; -import { LiveKernelModel } from './jupyter/kernels/types'; -import { suppressShutdownErrors } from './raw-kernel/rawKernel'; -import { IJupyterKernelSpec, 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 kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined; - protected interpreter: PythonInterpreter | undefined; - 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; - } - 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'); - - constructor(private restartSessionUsed: (id: Kernel.IKernelConnection) => void) { - this.statusHandler = this.onStatusChanged.bind(this); - } - 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( - kernel: IJupyterKernelSpec | LiveKernelModel, - timeoutMS: number, - interpreter?: PythonInterpreter - ): Promise { - let newSession: ISessionWithSocket | undefined; - - // If we are already using this kernel in an active session just return back - if (this.session && this.kernelSpec) { - // Name and id have to match (id is only for active sessions) - if (this.kernelSpec.name === kernel.name && this.kernelSpec.id === kernel.id) { - return; - } - } - - newSession = await this.createNewKernelSession(kernel, timeoutMS, interpreter); - - // 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 spec and interpreter - this.kernelSpec = kernel; - this.interpreter = interpreter; - - // 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(kernel, newSession, interpreter); - } - - 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.kernelSpec, 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( - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - session: ISessionWithSocket, - interpreter?: PythonInterpreter, - cancelToken?: CancellationToken - ): Promise; - - // Sub classes need to implement their own kernel change specific code - protected abstract createNewKernelSession( - kernel: IJupyterKernelSpec | LiveKernelModel, - timeoutMS: number, - interpreter?: PythonInterpreter - ): 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(); - reject( - new JupyterInvalidKernelError({ - ...session.kernel, - lastActivityTime: new Date(), - numberOfConnections: 0, - session: session.model - }) - ); - } - }; - - 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; - this._session = session; - - // 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 assocated 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 { - return 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 deleted file mode 100644 index b8df01e41317..000000000000 --- a/src/client/datascience/cellFactory.ts +++ /dev/null @@ -1,195 +0,0 @@ -// 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 deleted file mode 100644 index 1a6a61848983..000000000000 --- a/src/client/datascience/cellMatcher.ts +++ /dev/null @@ -1,89 +0,0 @@ -// 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 deleted file mode 100644 index b0232c8704c4..000000000000 --- a/src/client/datascience/codeCssGenerator.ts +++ /dev/null @@ -1,518 +0,0 @@ -// 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.readLocalData(theme.tokenColors); - tokenColors = JSON.parse(style.toString()); - } 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 deleted file mode 100644 index 0ea635d5c83d..000000000000 --- a/src/client/datascience/commands/commandLineSelector.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index 94c949f008fc..000000000000 --- a/src/client/datascience/commands/commandRegistry.ts +++ /dev/null @@ -1,459 +0,0 @@ -// 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.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 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/gathersurvey?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 deleted file mode 100644 index b8be186195e0..000000000000 --- a/src/client/datascience/commands/exportCommands.ts +++ /dev/null @@ -1,151 +0,0 @@ -// 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 deleted file mode 100644 index 1605043759b7..000000000000 --- a/src/client/datascience/commands/notebookCommands.ts +++ /dev/null @@ -1,89 +0,0 @@ -// 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 { KernelSelector, KernelSpecInterpreter } from '../jupyter/kernels/kernelSelector'; -import { KernelSwitcher } from '../jupyter/kernels/kernelSwitcher'; -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) - ); - } - public dispose() { - this.disposables.forEach((d) => d.dispose()); - } - 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: - this.interactiveWindowProvider.activeWindow?.notebook?.getKernelSpec()?.display_name || - this.interactiveWindowProvider.activeWindow?.notebook?.getKernelSpec()?.name - }; - } - 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: KernelSpecInterpreter, identity: Uri, resource: Uri | undefined) { - const specOrModel = kernel?.kernelModel || kernel?.kernelSpec; - if (kernel && 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 deleted file mode 100644 index ea2956f0d331..000000000000 --- a/src/client/datascience/commands/serverSelector.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index 87ade2bdb53a..000000000000 --- a/src/client/datascience/common.ts +++ /dev/null @@ -1,185 +0,0 @@ -// 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 * as path from 'path'; -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 }[] { - const uriList = globalState.get<{ uri: string; time: number }[]>(Settings.JupyterServerUriList); - return uriList - ? uriList.sort((a, b) => { - return b.time - a.time; - }) - : []; -} -export function addToUriList(globalState: Memento, uri: string, time: number) { - const uriList = getSavedUriList(globalState); - - const editList = uriList.filter((f, i) => { - return f.uri !== uri && i < Settings.JupyterServerUriListMax - 1; - }); - editList.splice(0, 0, { uri, time }); - - 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, 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 { - // Because of this bug here: - // https://github.com/microsoft/vscode/issues/93441 - // We can't create 'untitled' files anymore. The untitled scheme will just be ignored. - // Instead we need to create untitled files in the temp folder and force a saveas whenever they're - // saved. - const filePath = Uri.file(path.join(os.tmpdir(), fileName)); - return filePath.with({ scheme: 'untitled', path: filePath.fsPath }); - } -} - -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 deleted file mode 100644 index 3a4bebfd6dda..000000000000 --- a/src/client/datascience/constants.ts +++ /dev/null @@ -1,587 +0,0 @@ -// 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 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 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', - 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 CodeSnippits { - 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 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 deleted file mode 100644 index 103021f29481..000000000000 --- a/src/client/datascience/context/activeEditorContext.ts +++ /dev/null @@ -1,178 +0,0 @@ -// 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 deleted file mode 100644 index e738ec9b9186..000000000000 --- a/src/client/datascience/crossProcessLock.ts +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 16cd6db0bcfb..000000000000 --- a/src/client/datascience/data-viewing/dataViewer.ts +++ /dev/null @@ -1,199 +0,0 @@ -// 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, IWebPanelProvider, 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 { WebViewHost } from '../webViewHost'; -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 WebViewHost implements IDataViewer, IDisposable { - private dataProvider: IDataViewerDataProvider | undefined; - private rowsTimer: StopWatch | undefined; - private pendingRowsCount: number = 0; - private dataFrameInfoPromise: Promise | undefined; - - constructor( - @inject(IWebPanelProvider) provider: IWebPanelProvider, - @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 deleted file mode 100644 index 4c122def7f62..000000000000 --- a/src/client/datascience/data-viewing/dataViewerDependencyService.ts +++ /dev/null @@ -1,119 +0,0 @@ -// 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 { PythonInterpreter } 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?: PythonInterpreter, - 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?: PythonInterpreter, - 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?: PythonInterpreter, - 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 deleted file mode 100644 index ce64481e6c41..000000000000 --- a/src/client/datascience/data-viewing/dataViewerFactory.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index eebd6a42ec4b..000000000000 --- a/src/client/datascience/data-viewing/dataViewerMessageListener.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { IWebPanel, IWebPanelMessageListener } 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 IWebPanelMessageListener { - private disposedCallback: () => void; - private callback: (message: string, payload: any) => void; - private viewChanged: (panel: IWebPanel) => void; - - constructor( - callback: (message: string, payload: any) => void, - viewChanged: (panel: IWebPanel) => 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: IWebPanel) { - // 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 deleted file mode 100644 index 58a4982edea9..000000000000 --- a/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts +++ /dev/null @@ -1,113 +0,0 @@ -// 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 deleted file mode 100644 index b8b48b45a4b3..000000000000 --- a/src/client/datascience/data-viewing/jupyterVariableDataProviderFactory.ts +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index df3c97bc5cd5..000000000000 --- a/src/client/datascience/data-viewing/types.ts +++ /dev/null @@ -1,83 +0,0 @@ -// 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 deleted file mode 100644 index 6a0e714a1726..000000000000 --- a/src/client/datascience/dataScienceFileSystem.ts +++ /dev/null @@ -1,222 +0,0 @@ -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); - const result = await this.vscfs.readFile(uri); - const data = Buffer.from(result); - return data.toString(ENCODING); - } - - 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); - const data = typeof text === 'string' ? Buffer.from(text) : text; - await this.vscfs.writeFile(uri, data); - } - - // 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; - await 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 deleted file mode 100644 index cd63823c1778..000000000000 --- a/src/client/datascience/dataScienceSurveyBanner.ts +++ /dev/null @@ -1,169 +0,0 @@ -// 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 deleted file mode 100644 index 9109ca732d94..000000000000 --- a/src/client/datascience/datascience.ts +++ /dev/null @@ -1,122 +0,0 @@ -// 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 deleted file mode 100644 index 6642b3f44652..000000000000 --- a/src/client/datascience/debugLocationTracker.ts +++ /dev/null @@ -1,128 +0,0 @@ -// 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 deleted file mode 100644 index d31287410c1a..000000000000 --- a/src/client/datascience/debugLocationTrackerFactory.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index 7f9f14a9de8e..000000000000 --- a/src/client/datascience/editor-integration/cellhashprovider.ts +++ /dev/null @@ -1,443 +0,0 @@ -// 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 deleted file mode 100644 index a3ced5f49440..000000000000 --- a/src/client/datascience/editor-integration/codeLensFactory.ts +++ /dev/null @@ -1,497 +0,0 @@ -// 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 deleted file mode 100644 index fcf233ca91e6..000000000000 --- a/src/client/datascience/editor-integration/codelensprovider.ts +++ /dev/null @@ -1,206 +0,0 @@ -// 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 deleted file mode 100644 index 856108875330..000000000000 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ /dev/null @@ -1,1072 +0,0 @@ -// 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 { - // full cell range is selected now decide if expanding or contracting? - if (isAnchorLessThanActive && startCellIndex < endCellIndex) { - // anchor is above active, contract selection by cell below. - const newEndCell = cells[endCellIndex - 1]; - editor.selection = new Selection(startCell.range.start, newEndCell.range.end); - } else { - // anchor is below active, expand selection by cell above. - if (startCellIndex > 0) { - const aboveCell = cells[startCellIndex - 1]; - editor.selection = new Selection(endCell.range.end, aboveCell.range.start); - } - } - } - } - - @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 { - // 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) { - const extendCell = cells[endCellIndex + 1]; - editor.selection = new Selection(startCell.range.start, extendCell.range.end); - } - } else { - // anchor is below active, contract selection by cell above. - if (startCellIndex < endCellIndex) { - const contractCell = cells[startCellIndex + 1]; - editor.selection = new Selection(endCell.range.end, contractCell.range.start); - } - } - } - } - - @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'); - }); - } - - 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 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 deleted file mode 100644 index f85e2723ed7d..000000000000 --- a/src/client/datascience/editor-integration/decorator.ts +++ /dev/null @@ -1,133 +0,0 @@ -// 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 deleted file mode 100644 index 23723262de08..000000000000 --- a/src/client/datascience/editor-integration/hoverProvider.ts +++ /dev/null @@ -1,139 +0,0 @@ -// 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 deleted file mode 100644 index f0fa2f29cd95..000000000000 --- a/src/client/datascience/errorHandler/errorHandler.ts +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index 0e1b31b9adf4..000000000000 --- a/src/client/datascience/export/README.md +++ /dev/null @@ -1,13 +0,0 @@ - -## 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 deleted file mode 100644 index 1d54a7ba5284..000000000000 --- a/src/client/datascience/export/exportBase.ts +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 0ec028999e51..000000000000 --- a/src/client/datascience/export/exportDependencyChecker.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index d2d1e086efdc..000000000000 --- a/src/client/datascience/export/exportFileOpener.ts +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index e6457ed8e297..000000000000 --- a/src/client/datascience/export/exportManager.ts +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index c92436c295dd..000000000000 --- a/src/client/datascience/export/exportManagerFilePicker.ts +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index d12c92502402..000000000000 --- a/src/client/datascience/export/exportToHTML.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0b4b6afba335..000000000000 --- a/src/client/datascience/export/exportToPDF.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index b72168886123..000000000000 --- a/src/client/datascience/export/exportToPython.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 237b93ff4ca1..000000000000 --- a/src/client/datascience/export/exportUtil.ts +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index d1ec1dc8e484..000000000000 --- a/src/client/datascience/export/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 9defc63854e1..000000000000 --- a/src/client/datascience/gather/gatherListener.ts +++ /dev/null @@ -1,304 +0,0 @@ -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.handleMessage(message, payload, this.doGather); - break; - - case InteractiveWindowMessages.GatherCodeToScript: - 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): void { - this.gatherCodeInternal(payload).catch((err) => { - traceError(`Gather to Notebook error: ${err}`); - this.applicationShell.showErrorMessage(err); - }); - } - - private doGatherToScript(payload: ICell): void { - this.gatherCodeInternal(payload, true).catch((err) => { - traceError(`Gather to Script error: ${err}`); - this.applicationShell.showErrorMessage(err); - }); - } - - 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 deleted file mode 100644 index b4731e8a95e7..000000000000 --- a/src/client/datascience/gather/gatherLogger.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { inject, injectable } from 'inversify'; -// tslint:disable-next-line: no-require-imports -import cloneDeep = require('lodash/cloneDeep'); -import { extensions } from 'vscode'; -import { concatMultilineStringInput } from '../../../datascience-ui/common'; -import { traceError } 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(concatMultilineStringInput(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; - this.gather = api.getGatherProvider(); - } - } -} diff --git a/src/client/datascience/interactive-common/debugListener.ts b/src/client/datascience/interactive-common/debugListener.ts deleted file mode 100644 index c50fb893ced5..000000000000 --- a/src/client/datascience/interactive-common/debugListener.ts +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index 6dd99b2b41fc..000000000000 --- a/src/client/datascience/interactive-common/intellisense/conversion.ts +++ /dev/null @@ -1,348 +0,0 @@ -// 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 deleted file mode 100644 index 52bbc2afbf5e..000000000000 --- a/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts +++ /dev/null @@ -1,693 +0,0 @@ -// 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 deleted file mode 100644 index 54dc5a7e5c2e..000000000000 --- a/src/client/datascience/interactive-common/intellisense/intellisenseLine.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index 0b809350bd41..000000000000 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ /dev/null @@ -1,948 +0,0 @@ -// 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 { concatMultilineStringInput } 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 { PythonInterpreter } 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: PythonInterpreter | 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: concatMultilineStringInput(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, - concatMultilineStringInput(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, - concatMultilineStringInput(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: concatMultilineStringInput(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 deleted file mode 100644 index e8ae47b1bd06..000000000000 --- a/src/client/datascience/interactive-common/intellisense/wordHelper.ts +++ /dev/null @@ -1,170 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 deleted file mode 100644 index 39cdc9a8c6ac..000000000000 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ /dev/null @@ -1,1538 +0,0 @@ -// 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, - IWebPanelProvider, - 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 { - 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -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 { JupyterKernelPromiseFailedError } from '../jupyter/kernels/jupyterKernelPromiseFailedError'; -import { KernelSelector, KernelSpecInterpreter } from '../jupyter/kernels/kernelSelector'; -import { LiveKernelModel } from '../jupyter/kernels/types'; -import { CssMessages, SharedMessages } from '../messages'; -import { - CellState, - ICell, - ICodeCssGenerator, - IDataScienceErrorHandler, - IDataScienceFileSystem, - IInteractiveBase, - IInteractiveWindowInfo, - IInteractiveWindowListener, - IJupyterDebugger, - IJupyterKernelSpec, - IJupyterVariableDataProviderFactory, - IJupyterVariables, - IJupyterVariablesRequest, - IJupyterVariablesResponse, - IMessageCell, - INotebook, - INotebookExporter, - INotebookMetadataLive, - INotebookProvider, - INotebookProviderConnection, - InterruptResult, - IStatusProvider, - IThemeFinder, - WebViewViewChangeEventArgs -} from '../types'; -import { WebViewHost } from '../webViewHost'; -import { InteractiveWindowMessageListener } from './interactiveWindowMessageListener'; -import { serializeLanguageConfiguration } from './serialization'; - -export abstract class InteractiveBase extends WebViewHost 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: IWebPanelProvider, - 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 accurrately 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 accurrately 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( - kernelSpec: IJupyterKernelSpec | LiveKernelModel, - interpreter: PythonInterpreter | undefined - ): 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 { - 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 || - this._notebook?.getKernelSpec()?.display_name || - this._notebook?.getKernelSpec()?.name - }); - } - - 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.kernelSpec - ); - if (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 { - localizedUri = serverConnection.displayName; - - // Log this remote URI into our MRU list - addToUriList(this.globalStorage, localizedUri, Date.now()); - } - } - - return localizedUri; - } - - private async listenToNotebookEvents(notebook: INotebook): Promise { - const statusChangeHandler = async (status: ServerStatus) => { - const kernelSpec = notebook.getKernelSpec(); - const name = kernelSpec?.display_name || kernelSpec?.name || ''; - - await this.postMessage(InteractiveWindowMessages.UpdateKernel, { - jupyterServerStatus: status, - localizedUri: this.getServerUri(notebook.connection), - displayName: name, - language: translateKernelLanguageToMonaco(kernelSpec?.language ?? 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; kernel: KernelSpecInterpreter }): Promise { - const specOrModel = data.kernel.kernelModel || data.kernel.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: specOrModel?.display_name || specOrModel?.name || '', - language: translateKernelLanguageToMonaco(data.kernel.kernelSpec?.language ?? PYTHON_LANGUAGE) - }).ignoreErrors(); - - // Update our model - this.updateNotebookOptions(specOrModel, data.kernel.interpreter).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 qualifieid 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 resotre 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(kernel: IJupyterKernelSpec | LiveKernelModel) { - // Check if we are changing to LiveKernelModel - if (kernel.hasOwnProperty('numberOfConnections')) { - await this.addSysInfo(SysInfoReason.Connect); - } else { - await this.addSysInfo(SysInfoReason.New); - } - return this.updateNotebookOptions(kernel, this._notebook?.getMatchingInterpreter()); - } - - 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 deleted file mode 100644 index ecfe74e46191..000000000000 --- a/src/client/datascience/interactive-common/interactiveWindowMessageListener.ts +++ /dev/null @@ -1,107 +0,0 @@ -// 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, IWebPanel, IWebPanelMessageListener } 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 IWebPanelMessageListener { - private postOffice: PostOffice; - private disposedCallback: () => void; - private callback: (message: string, payload: any) => void; - private viewChanged: (panel: IWebPanel) => void; - private interactiveWindowMessages: string[] = []; - - constructor( - liveShare: ILiveShareApi, - callback: (message: string, payload: any) => void, - viewChanged: (panel: IWebPanel) => 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: IWebPanel) { - // 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 deleted file mode 100644 index 834332286ed7..000000000000 --- a/src/client/datascience/interactive-common/interactiveWindowTypes.ts +++ /dev/null @@ -1,685 +0,0 @@ -// 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, - ILoadIPyWidgetClassFailureAction, - IVariableExplorerHeight, - LoadIPyWidgetClassLoadAction, - NotifyIPyWidgeWidgetVersionNotSupportedAction -} from '../../../datascience-ui/interactive-common/redux/reducers/types'; -import { Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { NativeKeyboardCommandTelemetry, NativeMouseCommandTelemetry } from '../constants'; -import { WidgetScriptSource } from '../ipywidgets/types'; -import { LiveKernelModel } from '../jupyter/kernels/types'; -import { CssMessages, IGetCssRequest, IGetCssResponse, IGetMonacoThemeRequest, SharedMessages } from '../messages'; -import { IGetMonacoThemeResponse } from '../monacoMessages'; -import { - ICell, - IInteractiveWindowInfo, - IJupyterKernelSpec, - 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', - 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; - shouldShowTrustMessage?: 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'; - interpreter: PythonInterpreter | undefined; - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined; -} - -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.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 deleted file mode 100644 index a05eecac22a8..000000000000 --- a/src/client/datascience/interactive-common/linkProvider.ts +++ /dev/null @@ -1,130 +0,0 @@ -// 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 linkCommandWhitelist = [ - '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 (linkCommandWhitelist.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 deleted file mode 100644 index 0828604d3752..000000000000 --- a/src/client/datascience/interactive-common/notebookProvider.ts +++ /dev/null @@ -1,193 +0,0 @@ -// 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 { KernelSpecInterpreter } from '../jupyter/kernels/kernelSelector'; -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; kernel: KernelSpecInterpreter }>(); - 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: KernelSpecInterpreter) { - this._potentialKernelChanged.fire({ identity, 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 deleted file mode 100644 index c2f50a9ae286..000000000000 --- a/src/client/datascience/interactive-common/notebookServerProvider.ts +++ /dev/null @@ -1,218 +0,0 @@ -// 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 deleted file mode 100644 index cef91dcddaa7..000000000000 --- a/src/client/datascience/interactive-common/notebookUsageTracker.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index da853c44f6ea..000000000000 --- a/src/client/datascience/interactive-common/serialization.ts +++ /dev/null @@ -1,144 +0,0 @@ -// 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 deleted file mode 100644 index 7151c9ffd108..000000000000 --- a/src/client/datascience/interactive-common/showPlotListener.ts +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index face994ec0cf..000000000000 --- a/src/client/datascience/interactive-common/synchronization.ts +++ /dev/null @@ -1,302 +0,0 @@ -// 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.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 deleted file mode 100644 index 3c22935868a2..000000000000 --- a/src/client/datascience/interactive-common/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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 deleted file mode 100644 index 6920c74a21f1..000000000000 --- a/src/client/datascience/interactive-ipynb/autoSaveService.ts +++ /dev/null @@ -1,175 +0,0 @@ -// 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 deleted file mode 100644 index 7d0def08b1fa..000000000000 --- a/src/client/datascience/interactive-ipynb/digestStorage.ts +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index 78ca797e0866..000000000000 --- a/src/client/datascience/interactive-ipynb/nativeEditor.ts +++ /dev/null @@ -1,813 +0,0 @@ -// 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, - IWebPanelProvider, - 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -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, - IJupyterKernelSpec, - 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 { concatMultilineStringInput, splitMultilineString } from '../../../datascience-ui/common'; -import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; -import { isTestExecution, PYTHON_LANGUAGE } from '../../common/constants'; -import { EnableTrustedNotebooks } from '../../common/experiments/groups'; -import { translateKernelLanguageToMonaco } from '../common'; -import { IDataViewerFactory } from '../data-viewing/types'; -import { getCellHashProvider } from '../editor-integration/cellhashprovider'; -import { KernelSelector } from '../jupyter/kernels/kernelSelector'; -import { LiveKernelModel } 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: IWebPanelProvider, - 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, - private 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( - kernelSpec: IJupyterKernelSpec | LiveKernelModel, - interpreter: PythonInterpreter | undefined - ): Promise { - if (this.model) { - const change: NotebookModelChange = { - kind: 'version', - kernelSpec, - interpreter, - 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; - } - - 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); - } - } 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 interpreter = notebook.getMatchingInterpreter(); - const kernelSpec = notebook.getKernelSpec(); - this.model.update({ - source: 'user', - kind: 'version', - oldDirty: this.model.isDirty, - newDirty: this.model.isDirty, - interpreter, - kernelSpec - }); - } - - // 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() { - // Ask user for a save as dialog if no title - if (this.isUntitled) { - this.commandManager.executeCommand('workbench.action.files.saveAs', this.file); - } else { - 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 = concatMultilineStringInput(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 }); - - const shouldShowTrustMessage = await this.expService.inExperiment(EnableTrustedNotebooks.experiment); - return this.postMessage(InteractiveWindowMessages.LoadAllCells, { - cells, - isNotebookTrusted, - shouldShowTrustMessage - }); - } - - 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 = concatMultilineStringInput(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 deleted file mode 100644 index 20629faff4ae..000000000000 --- a/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts +++ /dev/null @@ -1,126 +0,0 @@ -// 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 deleted file mode 100644 index 2510048c538f..000000000000 --- a/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts +++ /dev/null @@ -1,309 +0,0 @@ -// 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, - IWebPanelProvider, - 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: IWebPanelProvider, - 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?.file); - 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.file, - 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 deleted file mode 100644 index c34be57da816..000000000000 --- a/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts +++ /dev/null @@ -1,401 +0,0 @@ -// 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, - IWebPanelProvider, - 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 { 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) private 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 - ); - - // 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 (resource: Uri) => { - const customDocument = this.customDocuments.get(resource.fsPath); - if (customDocument) { - await this.saveCustomDocument(customDocument, new CancellationTokenSource().token); - } - }) - ); - this.disposables.push( - this.cmdManager.registerCommand( - Commands.SaveAsNotebookNonCustomEditor, - async (resource: Uri, targetResource: Uri) => { - const customDocument = this.customDocuments.get(resource.fsPath); - if (customDocument) { - await this.saveCustomDocumentAs(customDocument, targetResource); - this.customDocuments.delete(resource.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(IWebPanelProvider), - 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; - } - - 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 deleted file mode 100644 index fc2860cae74f..000000000000 --- a/src/client/datascience/interactive-ipynb/nativeEditorRunByLineListener.ts +++ /dev/null @@ -1,134 +0,0 @@ -// 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 deleted file mode 100644 index 74d7ecd1c734..000000000000 --- a/src/client/datascience/interactive-ipynb/nativeEditorSynchronizer.ts +++ /dev/null @@ -1,41 +0,0 @@ -// 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 deleted file mode 100644 index b80b4ea5365e..000000000000 --- a/src/client/datascience/interactive-ipynb/nativeEditorViewTracker.ts +++ /dev/null @@ -1,78 +0,0 @@ -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 { isUntitled } from '../notebookStorage/nativeEditorStorage'; -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) && (!isUntitled(editor.model) || editor.isDirty)) { - this.workspaceMemento.update(MEMENTO_KEY, [...list, fileKey]); - } else if (isUntitled(editor.model) && 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 deleted file mode 100644 index 12aec409f0ff..000000000000 --- a/src/client/datascience/interactive-ipynb/trustCommandHandler.ts +++ /dev/null @@ -1,82 +0,0 @@ -// 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 { EnableTrustedNotebooks } from '../../common/experiments/groups'; -import '../../common/extensions'; -import { IDisposableRegistry, IExperimentService } 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, - @inject(IExperimentService) private readonly experiments: IExperimentService - ) {} - public async activate(): Promise { - this.activateInBackground().ignoreErrors(); - } - public async activateInBackground(): Promise { - if (!(await this.experiments.inExperiment(EnableTrustedNotebooks.experiment))) { - return; - } - 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 deleted file mode 100644 index 47380d9c23ee..000000000000 --- a/src/client/datascience/interactive-ipynb/trustService.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { createHmac } from 'crypto'; -import { inject, injectable } from 'inversify'; -import { EventEmitter, Uri } from 'vscode'; -import { EnableTrustedNotebooks } from '../../common/experiments/groups'; -import { IConfigurationService, IExperimentService } 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(); - private enabled: Promise; - constructor( - @inject(IExperimentService) private readonly experimentService: IExperimentService, - @inject(IDigestStorage) private readonly digestStorage: IDigestStorage, - @inject(IConfigurationService) private configService: IConfigurationService - ) { - this.enabled = this.isInExperiment(); - } - - /** - * 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 || !(await this.enabled)) { - return true; // Skip check if user manually overrode our trust checking, or if user is not in experiment - } - // 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 && (await this.enabled)) { - // 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'); - } - - private async isInExperiment() { - return this.experimentService.inExperiment(EnableTrustedNotebooks.experiment); - } -} diff --git a/src/client/datascience/interactive-window/identity.ts b/src/client/datascience/interactive-window/identity.ts deleted file mode 100644 index 745630a85ae1..000000000000 --- a/src/client/datascience/interactive-window/identity.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index 4dcc3ab5f1f8..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ /dev/null @@ -1,541 +0,0 @@ -// 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, - IWebPanelProvider, - 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -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 { - ICell, - ICodeCssGenerator, - IDataScienceErrorHandler, - IDataScienceFileSystem, - IInteractiveWindow, - IInteractiveWindowInfo, - IInteractiveWindowListener, - IInteractiveWindowLoadable, - IInteractiveWindowProvider, - IJupyterDebugger, - IJupyterKernelSpec, - 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: IWebPanelProvider, - 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( - _kernelSpec: IJupyterKernelSpec, - _interpreter: PythonInterpreter | undefined - ): 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 deleted file mode 100644 index f68d89c200c1..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +++ /dev/null @@ -1,499 +0,0 @@ -// 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 deleted file mode 100644 index 06925194011e..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindowProvider.ts +++ /dev/null @@ -1,338 +0,0 @@ -// 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, - IWebPanelProvider, - 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(IWebPanelProvider), - 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 deleted file mode 100644 index 47937d0970e6..000000000000 --- a/src/client/datascience/ipywidgets/cdnWidgetScriptSourceProvider.ts +++ /dev/null @@ -1,246 +0,0 @@ -// 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 deleted file mode 100644 index a413d7ec7490..000000000000 --- a/src/client/datascience/ipywidgets/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 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 deleted file mode 100644 index 71d7bbd61d76..000000000000 --- a/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcher.ts +++ /dev/null @@ -1,504 +0,0 @@ -// 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 deleted file mode 100644 index 5519cfad3439..000000000000 --- a/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcherFactory.ts +++ /dev/null @@ -1,143 +0,0 @@ -// 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 deleted file mode 100644 index c2882ba32267..000000000000 --- a/src/client/datascience/ipywidgets/ipyWidgetScriptSource.ts +++ /dev/null @@ -1,310 +0,0 @@ -// 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 { PythonInterpreter } 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?: PythonInterpreter; - /** - * 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 deleted file mode 100644 index 13b6c3fb8871..000000000000 --- a/src/client/datascience/ipywidgets/ipyWidgetScriptSourceProvider.ts +++ /dev/null @@ -1,246 +0,0 @@ -// 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 deleted file mode 100644 index c2f569070d80..000000000000 --- a/src/client/datascience/ipywidgets/ipywidgetHandler.ts +++ /dev/null @@ -1,185 +0,0 @@ -// 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 deleted file mode 100644 index d6e69a5621e3..000000000000 --- a/src/client/datascience/ipywidgets/localWidgetScriptSourceProvider.ts +++ /dev/null @@ -1,113 +0,0 @@ -// 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry } from '../../telemetry'; -import { Telemetry } from '../constants'; -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 interpreter = this.getInterpreter(); - if (interpreter?.sysPrefix) { - return interpreter?.sysPrefix; - } - if (!interpreter?.path) { - return; - } - const interpreterInfo = await this.interpreterService - .getInterpreterDetails(interpreter.path) - .catch(traceError.bind(`Failed to get interpreter details for Kernel/Interpreter ${interpreter.path}`)); - - if (interpreterInfo) { - return interpreterInfo?.sysPrefix; - } - } - private getInterpreter(): Partial | undefined { - let interpreter: undefined | Partial = this.notebook.getMatchingInterpreter(); - const kernel = this.notebook.getKernelSpec(); - interpreter = kernel?.metadata?.interpreter?.path ? kernel?.metadata?.interpreter : interpreter; - - // If we still do not have the interpreter, then check if we have the path to the kernel. - if (!interpreter && kernel?.path) { - interpreter = { path: kernel.path }; - } - - if (!interpreter || !interpreter.path) { - return; - } - const pythonPath = interpreter.path; - return { ...interpreter, path: pythonPath }; - } -} diff --git a/src/client/datascience/ipywidgets/remoteWidgetScriptSourceProvider.ts b/src/client/datascience/ipywidgets/remoteWidgetScriptSourceProvider.ts deleted file mode 100644 index 194c9ee00b67..000000000000 --- a/src/client/datascience/ipywidgets/remoteWidgetScriptSourceProvider.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index e09243ffa1fa..000000000000 --- a/src/client/datascience/ipywidgets/types.ts +++ /dev/null @@ -1,51 +0,0 @@ -// 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 deleted file mode 100644 index 9b8c8dfc1b8b..000000000000 --- a/src/client/datascience/jupyter/commandLineSelector.ts +++ /dev/null @@ -1,116 +0,0 @@ -// 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 deleted file mode 100644 index 176b1a622936..000000000000 --- a/src/client/datascience/jupyter/debuggerVariableRegistration.ts +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index 6b4093774ad1..000000000000 --- a/src/client/datascience/jupyter/debuggerVariables.ts +++ /dev/null @@ -1,312 +0,0 @@ -// 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 deleted file mode 100644 index c8f28751630c..000000000000 --- a/src/client/datascience/jupyter/interpreter/README.md +++ /dev/null @@ -1 +0,0 @@ -# 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 deleted file mode 100644 index e5e78b220c82..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterCommand.ts +++ /dev/null @@ -1,339 +0,0 @@ -// 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 { PythonInterpreter } 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: PythonInterpreter, - 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: PythonInterpreter, - 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: PythonInterpreter, - 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: PythonInterpreter, 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: PythonInterpreter, 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: PythonInterpreter, - 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 deleted file mode 100644 index fb6e38e85082..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts +++ /dev/null @@ -1,350 +0,0 @@ -// 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 { PythonInterpreter } 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 {PythonInterpreter} interpreter - * @param {JupyterInstallError} [_error] - * @param {CancellationToken} [token] - * @returns {Promise} - * @memberof JupyterInterpreterDependencyService - */ - @reportAction(ReportableAction.InstallingMissingDependencies) - public async installMissingDependencies( - interpreter: PythonInterpreter, - _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 {PythonInterpreter} interpreter - * @param {CancellationToken} [token] - * @returns {Promise} - * @memberof JupyterInterpreterConfigurationService - */ - public async areDependenciesInstalled(interpreter: PythonInterpreter, 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 {PythonInterpreter} interpreter - * @param {CancellationToken} [_token] - * @returns {Promise} - * @memberof JupyterInterpreterConfigurationService - */ - public async isExportSupported(interpreter: PythonInterpreter, _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 {PythonInterpreter} interpreter - * @param {CancellationToken} [token] - * @returns {Promise} - * @memberof JupyterInterpreterConfigurationService - */ - public async getDependenciesNotInstalled( - interpreter: PythonInterpreter, - 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 {PythonInterpreter} interpreter - * @param {CancellationToken} [_token] - * @returns {Promise} - * @memberof JupyterInterpreterConfigurationService - */ - private async isKernelSpecAvailable(interpreter: PythonInterpreter, _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 {PythonInterpreter} interpreter - * @param {CancellationToken} [token] - * @returns {Promise} - * @memberof JupyterInterpreterConfigurationService - */ - private async checkKernelSpecAvailability( - interpreter: PythonInterpreter, - 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 deleted file mode 100644 index 86b8a4d26889..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index 7ee3aaf42cf0..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index 19e520d3298b..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelector.ts +++ /dev/null @@ -1,63 +0,0 @@ -// 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 { PythonInterpreter } 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 deleted file mode 100644 index 8b330e3c7aac..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts +++ /dev/null @@ -1,236 +0,0 @@ -// 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 { PythonInterpreter } 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?: PythonInterpreter; - 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: PythonInterpreter): 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: PythonInterpreter) { - 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: PythonInterpreter | 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 deleted file mode 100644 index 3fecb01848db..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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 deleted file mode 100644 index 858c27b117a7..000000000000 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts +++ /dev/null @@ -1,275 +0,0 @@ -// 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 { PythonInterpreter } 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 deleted file mode 100644 index eb8989a55409..000000000000 --- a/src/client/datascience/jupyter/invalidNotebookFileError.ts +++ /dev/null @@ -1,15 +0,0 @@ -// 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 deleted file mode 100644 index b595b232341f..000000000000 --- a/src/client/datascience/jupyter/jupyterCellOutputMimeTypeTracker.ts +++ /dev/null @@ -1,149 +0,0 @@ -// 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 deleted file mode 100644 index 9cd33eae3a3b..000000000000 --- a/src/client/datascience/jupyter/jupyterConnectError.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 deleted file mode 100644 index 5b1dd85ac75f..000000000000 --- a/src/client/datascience/jupyter/jupyterConnection.ts +++ /dev/null @@ -1,266 +0,0 @@ -// 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 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, 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, - 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 deleted file mode 100644 index 7e08688651dd..000000000000 --- a/src/client/datascience/jupyter/jupyterDataRateLimitError.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 deleted file mode 100644 index 5e21db77253d..000000000000 --- a/src/client/datascience/jupyter/jupyterDebugger.ts +++ /dev/null @@ -1,554 +0,0 @@ -// 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 { concatMultilineStringOutput } 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 concatMultilineStringOutput(stream.text); - } - } - } - 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 deleted file mode 100644 index 755d5f208ee1..000000000000 --- a/src/client/datascience/jupyter/jupyterDebuggerNotInstalledError.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index 8ff97f941dda..000000000000 --- a/src/client/datascience/jupyter/jupyterDebuggerPortBlockedError.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index 1a33e9eb00ec..000000000000 --- a/src/client/datascience/jupyter/jupyterDebuggerPortNotAvailableError.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index 03c09ef8d1dd..000000000000 --- a/src/client/datascience/jupyter/jupyterDebuggerRemoteNotSupported.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 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 deleted file mode 100644 index 1fd947d3c40c..000000000000 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ /dev/null @@ -1,441 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -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 { PythonInterpreter } 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 } from './jupyterUtils'; -import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; -import { KernelSelector, KernelSpecInterpreter } from './kernels/kernelSelector'; -import { NotebookStarter } from './notebookStarter'; - -const LocalHosts = ['localhost', '127.0.0.1', '::1']; - -export class JupyterExecutionBase implements IJupyterExecution { - private usablePythonInterpreter: PythonInterpreter | 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, - 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 kernelSpecInterpreter: KernelSpecInterpreter | undefined; - let kernelSpecInterpreterPromise: Promise = Promise.resolve({}); - 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`); - kernelSpecInterpreterPromise = this.kernelSelector.getKernelForLocalConnection( - 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, kernelSpecInterpreter] = await Promise.all([ - this.startOrConnect(options, cancelToken), - kernelSpecInterpreterPromise - ]); - - 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 (!kernelSpecInterpreter.kernelSpec && connection && !options?.skipSearchingForKernel) { - const sessionManagerFactory = this.serviceContainer.get( - IJupyterSessionManagerFactory - ); - const sessionManager = await sessionManagerFactory.create(connection); - kernelSpecInterpreter = await this.kernelSelector.getKernelForRemoteConnection( - undefined, - sessionManager, - options?.metadata, - cancelToken - ); - await sessionManager.dispose(); - } - - // If no kernel and not going to pick one, exit early - if (!Object.keys(kernelSpecInterpreter) && !allowUI) { - return undefined; - } - - // Populate the launch info that we are starting our server with - const launchInfo: INotebookServerLaunchInfo = { - connectionInfo: connection!, - interpreter: kernelSpecInterpreter.interpreter, - kernelSpec: kernelSpecInterpreter.kernelSpec, - 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( - launchInfo.kernelSpec?.display_name || launchInfo.kernelSpec?.name || '', - 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, - launchInfo.kernelSpec?.display_name || launchInfo.kernelSpec?.name - ); - if (Object.keys(kernelInterpreter).length > 0) { - launchInfo.interpreter = kernelInterpreter.interpreter; - launchInfo.kernelSpec = - kernelInterpreter.kernelSpec || kernelInterpreter.kernelModel; - 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; - const connection = await this.startNotebookServer( - useDefaultConfig, - this.configuration.getSettings(undefined).datascience.jupyterCommandLineArguments, - 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[], - cancelToken?: CancellationToken - ): Promise { - return this.notebookStarter.start(useDefaultConfig, customCommandLine, 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 deleted file mode 100644 index 6c5fb1abc9f6..000000000000 --- a/src/client/datascience/jupyter/jupyterExecutionFactory.ts +++ /dev/null @@ -1,159 +0,0 @@ -// 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 { PythonInterpreter } 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 deleted file mode 100644 index 6036f1d6cce2..000000000000 --- a/src/client/datascience/jupyter/jupyterExporter.ts +++ /dev/null @@ -1,268 +0,0 @@ -// 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 { concatMultilineStringInput } 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 { CodeSnippits, 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, - kernelspec: kernelSpec - }; - - // 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 = CodeSnippits.ChangeDirectory.join(os.EOL).format( - localize.DataScience.exportChangeDirectoryComment(), - CodeSnippits.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) => - concatMultilineStringInput(c.data.source).includes(CodeSnippits.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 deleted file mode 100644 index 93cbf4fccad4..000000000000 --- a/src/client/datascience/jupyter/jupyterImporter.ts +++ /dev/null @@ -1,177 +0,0 @@ -// 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 { CodeSnippits, 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 CodeSnippits.ImportIPython.format(this.defaultCellMarker, pythonOutput); - }; - - private addDirectoryChange = (pythonOutput: string, directoryChange: string): string => { - const newCode = CodeSnippits.ChangeDirectory.join(os.EOL).format( - localize.DataScience.importChangeDirectoryComment().format(this.defaultCellMarker), - CodeSnippits.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(CodeSnippits.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 deleted file mode 100644 index 6af845382351..000000000000 --- a/src/client/datascience/jupyter/jupyterInstallError.ts +++ /dev/null @@ -1,16 +0,0 @@ -// 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 deleted file mode 100644 index 172899250d3d..000000000000 --- a/src/client/datascience/jupyter/jupyterInterruptError.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index 46b80706af79..000000000000 --- a/src/client/datascience/jupyter/jupyterInvalidKernelError.ts +++ /dev/null @@ -1,19 +0,0 @@ -// 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 { IJupyterKernelSpec } from '../types'; -import { LiveKernelModel } from './kernels/types'; - -export class JupyterInvalidKernelError extends Error { - constructor(private _kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined) { - super(localize.DataScience.kernelInvalid().format(_kernelSpec?.display_name || _kernelSpec?.name || '')); - sendTelemetryEvent(Telemetry.KernelInvalid); - } - - public get kernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined { - return this._kernelSpec; - } -} diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts deleted file mode 100644 index 24fbfa618299..000000000000 --- a/src/client/datascience/jupyter/jupyterNotebook.ts +++ /dev/null @@ -1,1430 +0,0 @@ -// 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { generateCells } from '../cellFactory'; -import { CellMatcher } from '../cellMatcher'; -import { CodeSnippits, Identifiers, Telemetry } from '../constants'; -import { - CellState, - ICell, - IDataScienceFileSystem, - IJupyterKernelSpec, - IJupyterSession, - INotebook, - INotebookCompletion, - INotebookExecutionInfo, - INotebookExecutionLogger, - InterruptResult, - KernelSocketInformation -} from '../types'; -import { expandWorkingDir } from './jupyterUtils'; -import { LiveKernelModel } from './kernels/types'; - -// tslint:disable-next-line: no-require-imports -import cloneDeep = require('lodash/cloneDeep'); -import { - concatMultilineStringInput, - concatMultilineStringOutput, - formatStreamText -} from '../../../datascience-ui/common'; -import { PYTHON_LANGUAGE } from '../../common/constants'; -import { RefBool } from '../../common/refBool'; - -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.disposed.event; - } - public get onKernelChanged(): Event { - return this.kernelChanged.event; - } - 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 disposed = 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; - } - - constructor( - _liveShare: ILiveShareApi, // This is so the liveshare mixin works - private 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.disposed.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 ? CodeSnippits.ConfigSvg : CodeSnippits.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(): PythonInterpreter | undefined { - return ( - this._executionInfo.interpreter || - (this._executionInfo.kernelSpec?.metadata?.interpreter as PythonInterpreter) - ); - } - - public getKernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined { - return this._executionInfo.kernelSpec; - } - - public async setKernelSpec( - spec: IJupyterKernelSpec | LiveKernelModel, - timeoutMS: number, - interpreter: PythonInterpreter | undefined - ): 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(spec, timeoutMS, interpreter); - - // Change our own kernel spec - // Only after session was successfully created. - this._executionInfo.kernelSpec = spec; - - // Rerun our initial setup - await this.initialize(); - } else { - // Change our own kernel spec - this._executionInfo.kernelSpec = spec; - } - - this.kernelChanged.fire(spec); - - // If our new kernelspec has an interpreter, set that as our interpreter too - if (interpreter) { - this._executionInfo.interpreter = interpreter; - } - } - - 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 - ? CodeSnippits.MatplotLibInitSvg - : CodeSnippits.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(concatMultilineStringOutput(stream.text))); - } 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 - ): 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 finda future for a previous cell. - ) - : 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 && - this._executionInfo.kernelSpec?.language === PYTHON_LANGUAGE && - (await this.fs.localDirectoryExists(directory)) - ) { - await this.executeSilently(CodeSnippits.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(concatMultilineStringInput(subscriber.cell.data.source), silent); - - // 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(concatMultilineStringOutput(existing.text)); - originalTextLength = originalText.length; - existing.text = trimFunc(originalText); - trimmedTextLength = existing.text.length; - } else { - const originalText = formatStreamText(concatMultilineStringOutput(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 deleted file mode 100644 index c1103cab0fb2..000000000000 --- a/src/client/datascience/jupyter/jupyterNotebookProvider.ts +++ /dev/null @@ -1,58 +0,0 @@ -// 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 deleted file mode 100644 index 0f1850218be8..000000000000 --- a/src/client/datascience/jupyter/jupyterPasswordConnect.ts +++ /dev/null @@ -1,461 +0,0 @@ -// 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 deleted file mode 100644 index 027103fb8d87..000000000000 --- a/src/client/datascience/jupyter/jupyterRequest.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 0c2ee41a5ae9..000000000000 --- a/src/client/datascience/jupyter/jupyterSelfCertsError.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 deleted file mode 100644 index f2167d7744e9..000000000000 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ /dev/null @@ -1,268 +0,0 @@ -// 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'; - -// 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 ${launchInfo.kernelSpec ? launchInfo.kernelSpec.name : '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.kernelSpec, 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) { - 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'); - } - - private async destroyKernelSpec() { - if (this.launchInfo) { - this.launchInfo.kernelSpec = 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 deleted file mode 100644 index 426b1331ec31..000000000000 --- a/src/client/datascience/jupyter/jupyterServerWrapper.ts +++ /dev/null @@ -1,157 +0,0 @@ -// 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 deleted file mode 100644 index 404d0353592a..000000000000 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ /dev/null @@ -1,187 +0,0 @@ -// 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 * 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -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, IJupyterKernelSpec, ISessionWithSocket } from '../types'; -import { JupyterInvalidKernelError } from './jupyterInvalidKernelError'; -import { JupyterWebSockets } from './jupyterWebSocket'; -import { LiveKernelModel } from './kernels/types'; - -export class JupyterSession extends BaseJupyterSession { - constructor( - private connInfo: IJupyterConnection, - private serverSettings: ServerConnection.ISettings, - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - private sessionManager: SessionManager, - private contentsManager: ContentsManager, - private readonly outputChannel: IOutputChannel, - private readonly restartSessionCreated: (id: Kernel.IKernelConnection) => void, - restartSessionUsed: (id: Kernel.IKernelConnection) => void - ) { - super(restartSessionUsed); - this.kernelSpec = 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.kernelSpec, timeoutMs, undefined, 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( - kernel: IJupyterKernelSpec | LiveKernelModel | undefined, - timeoutMS: number, - _pythonInterpreter?: PythonInterpreter, - cancelToken?: CancellationToken - ): Promise { - let newSession: ISessionWithSocket | undefined; - - try { - // Don't immediately assume this kernel is valid. Try creating a session with it first. - if (kernel && kernel.id && 'session' in kernel) { - // Remote case. - newSession = this.sessionManager.connectTo(kernel.session); - newSession.isRemoteSession = true; - } else { - newSession = await this.createSession(this.serverSettings, kernel, this.contentsManager, 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(kernel); - } - - return newSession; - } - - protected async createRestartSession( - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - session: ISessionWithSocket, - _interpreter?: PythonInterpreter, - 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, - kernelSpec, - this.contentsManager, - cancelToken - ); - await this.waitForIdleOnSession(result, 30000); - 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.kernelSpec, this.session); - } - } - - private async createSession( - serverSettings: ServerConnection.ISettings, - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - contentsManager: ContentsManager, - cancelToken?: CancellationToken - ): Promise { - // Create a temporary notebook for this session. - const backingFile = await contentsManager.newUntitled({ type: 'notebook' }); - - // Create our session options using this temporary notebook and our connection info - const options: Session.IOptions = { - path: backingFile.path, - kernelName: kernelSpec ? kernelSpec.name : '', - 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.connInfo.localLaunch) { - 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 deleted file mode 100644 index 3e8094b2ff94..000000000000 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ /dev/null @@ -1,387 +0,0 @@ -// 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 { LiveKernelModel } 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( - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - 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, - kernelSpec, - this.sessionManager, - this.contentsManager, - this.outputChannel, - this.restartSessionCreatedEvent.fire.bind(this.restartSessionCreatedEvent), - this.restartSessionUsedEvent.fire.bind(this.restartSessionUsedEvent) - ); - 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 deleted file mode 100644 index 25a991fb4572..000000000000 --- a/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts +++ /dev/null @@ -1,69 +0,0 @@ -// 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 deleted file mode 100644 index 0bf0a2364b96..000000000000 --- a/src/client/datascience/jupyter/jupyterUtils.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import * as path from 'path'; -import { Uri } from 'vscode'; - -import { IWorkspaceService } from '../../common/application/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, - workspace: IWorkspaceService -): string { - if (workingDir) { - const variables = new SystemVariables(Uri.file(launchingFile), undefined, workspace); - return variables.resolve(workingDir); - } - - // No working dir, just use the path of the launching file. - return path.dirname(launchingFile); -} - -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, - getAuthHeader: serverUri ? () => getJupyterServerUri(uri)?.authorizationHeader : undefined - }; -} diff --git a/src/client/datascience/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts deleted file mode 100644 index a9cf9958a118..000000000000 --- a/src/client/datascience/jupyter/jupyterVariables.ts +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index b02bab07b807..000000000000 --- a/src/client/datascience/jupyter/jupyterWaitForIdleError.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index f11ed9281294..000000000000 --- a/src/client/datascience/jupyter/jupyterWebSocket.ts +++ /dev/null @@ -1,64 +0,0 @@ -// 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 deleted file mode 100644 index 96a5e47107aa..000000000000 --- a/src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index 6236de1f1914..000000000000 --- a/src/client/datascience/jupyter/kernelVariables.ts +++ /dev/null @@ -1,461 +0,0 @@ -// 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'; - -// 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 - let language = PYTHON_LANGUAGE; - if (notebook) { - const kernel = notebook.getKernelSpec(); - language = kernel && kernel.language ? kernel.language : 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 && - notebook.getKernelSpec()?.language === 'python' && - 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/helpers.ts b/src/client/datascience/jupyter/kernels/helpers.ts deleted file mode 100644 index 379dfe1fd5ee..000000000000 --- a/src/client/datascience/jupyter/kernels/helpers.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import type { Kernel } from '@jupyterlab/services'; -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'); - -// 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); -} - -// 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); -} - -// 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 deleted file mode 100644 index 6ba8afa63045..000000000000 --- a/src/client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index 5477f19b35ac..000000000000 --- a/src/client/datascience/jupyter/kernels/jupyterKernelSpec.ts +++ /dev/null @@ -1,84 +0,0 @@ -// 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 { PythonInterpreter } 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/kernelDependencyService.ts b/src/client/datascience/jupyter/kernels/kernelDependencyService.ts deleted file mode 100644 index 47b00676f6cb..000000000000 --- a/src/client/datascience/jupyter/kernels/kernelDependencyService.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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 { PythonInterpreter } 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: PythonInterpreter, - 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: PythonInterpreter, _token?: CancellationToken): Promise { - return this.installer.isInstalled(Product.ipykernel, interpreter).then((installed) => installed === true); - } -} diff --git a/src/client/datascience/jupyter/kernels/kernelSelections.ts b/src/client/datascience/jupyter/kernels/kernelSelections.ts deleted file mode 100644 index f81cad024d8c..000000000000 --- a/src/client/datascience/jupyter/kernels/kernelSelections.ts +++ /dev/null @@ -1,310 +0,0 @@ -// 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 } from 'vscode'; - -import { IPathUtils, Resource } from '../../../common/types'; -import * as localize from '../../../common/utils/localize'; -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, LiveKernelModel } 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 } - }; -} - -/** - * 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, kernelSpec: undefined, interpreter: undefined } - }; -} - -/** - * 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 | undefined - ): 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 } - }; - }); - } -} - -/** - * Provides a list of kernel specs for selection, for both local and remote sessions. - * - * @export - * @class KernelSelectionProviderFactory - */ -@injectable() -export class KernelSelectionProvider { - private localSuggestionsCache: IKernelSpecQuickPickItem[] = []; - private remoteSuggestionsCache: IKernelSpecQuickPickItem[] = []; - 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]); - - // Sorty 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.localSuggestionsCache = items)); - // If we have someting 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 = 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]; - // Sorty 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 someting in cache, return that, while fetching in the background. - const cachedItems = - this.localSuggestionsCache.length > 0 ? Promise.resolve(this.localSuggestionsCache) : liveItems; - return Promise.race([cachedItems, liveItems]); - } -} diff --git a/src/client/datascience/jupyter/kernels/kernelSelector.ts b/src/client/datascience/jupyter/kernels/kernelSelector.ts deleted file mode 100644 index 2a591b13f99a..000000000000 --- a/src/client/datascience/jupyter/kernels/kernelSelector.ts +++ /dev/null @@ -1,629 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import '../../../common/extensions'; - -import type { nbformat } from '@jupyterlab/coreutils'; -import type { Kernel } from '@jupyterlab/services'; -import { inject, injectable } from 'inversify'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { IApplicationShell } from '../../../common/application/types'; -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 { PythonInterpreter } 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 } from './helpers'; -import { KernelSelectionProvider } from './kernelSelections'; -import { KernelService } from './kernelService'; -import { IKernelSpecQuickPickItem, LiveKernelModel } from './types'; - -export type KernelSpecInterpreter = { - kernelSpec?: IJupyterKernelSpec; - /** - * Interpreter that goes with the kernelspec. - * Sometimes, we're unable to determine the exact interpreter associalted with a kernelspec, in such cases this is a closes match. - * E.g. when selecting a remote kernel, we do not have the remote interpreter information, we can only try to find a close match. - * - * @type {PythonInterpreter} - */ - interpreter?: PythonInterpreter; - /** - * Active kernel from an active session. - * If this is available, then user needs to connect to an existing kernel (instead of starting a new session). - * - * @type {(LiveKernelModel)} - */ - kernelModel?: LiveKernelModel; -}; - -@injectable() -export class KernelSelector { - /** - * 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 kernelDepdencyService: 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. - * - * @param {Resource} resource - * @param {StopWatch} stopWatch - * @param {IJupyterSessionManager} session - * @param {CancellationToken} [cancelToken] - * @param {IJupyterKernelSpec | LiveKernelModel} [currentKernel] - * @returns {Promise} - * @memberof KernelSelector - */ - 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 || '')); - return this.selectKernel( - resource, - 'jupyter', - stopWatch, - Telemetry.SelectRemoteJupyterKernel, - suggestions, - session, - cancelToken, - currentKernelDisplayName - ); - } - /** - * Select a kernel from a local session. - * - * @param {Resource} resource - * @param type - * @param {StopWatch} stopWatch - * @param {IJupyterSessionManager} [session] - * @param {CancellationToken} [cancelToken] - * @param {IJupyterKernelSpec | LiveKernelModel} [currentKernel] - * @returns {Promise} - * @memberof KernelSelector - */ - public async selectLocalKernel( - resource: Resource, - type: 'raw' | 'jupyter' | 'noConnection', - stopWatch: StopWatch, - session?: IJupyterSessionManager, - cancelToken?: CancellationToken, - currentKernelDisplayName?: string - ): Promise { - let suggestions = await this.selectionProvider.getKernelSelectionsForLocalSession( - resource, - type, - session, - cancelToken - ); - suggestions = suggestions.filter((item) => !this.kernelIdsToHide.has(item.selection.kernelModel?.id || '')); - return this.selectKernel( - resource, - type, - stopWatch, - Telemetry.SelectLocalJupyterKernel, - suggestions, - session, - cancelToken, - currentKernelDisplayName - ); - } - /** - * 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). - * - * @param {Resource} resource - * @param type - * @param {IJupyterSessionManager} [sessionManager] - * @param {nbformat.INotebookMetadata} [notebookMetadata] - * @param {boolean} [disableUI] - * @param {CancellationToken} [cancelToken] - * @returns {Promise} - * @memberof KernelSelector - */ - @reportAction(ReportableAction.KernelsGetKernelForLocalConnection) - public async getKernelForLocalConnection( - resource: Resource, - type: 'raw' | 'jupyter' | 'noConnection', - sessionManager?: IJupyterSessionManager, - notebookMetadata?: nbformat.INotebookMetadata, - disableUI?: boolean, - cancelToken?: CancellationToken - ): Promise { - 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: KernelSpecInterpreter = {}; - - 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); - } - - // If still not found, log an error (this seems possible for some people, so use the default) - if (!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); - return selection; - } - - /** - * 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). - * - * @param {Resource} resource - * @param {IJupyterSessionManager} [sessionManager] - * @param {nbformat.INotebookMetadata} [notebookMetadata] - * @param {CancellationToken} [cancelToken] - * @returns {Promise} - * @memberof KernelSelector - */ - // tslint:disable-next-line: cyclomatic-complexity - @reportAction(ReportableAction.KernelsGetKernelForRemoteConnection) - public async getKernelForRemoteConnection( - 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 { - kernelModel: { ...session.kernel, lastActivityTime, numberOfConnections, session }, - interpreter: interpreter - }; - } - } - - // 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; - } - } - - return { - kernelSpec: bestMatch, - interpreter: interpreter - }; - } - - public async askForLocalKernel( - resource: Resource, - type: 'raw' | 'jupyter' | 'noConnection', - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined - ): Promise { - const displayName = kernelSpec?.display_name || kernelSpec?.name || ''; - 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) { - return this.selectLocalJupyterKernel(resource, type, kernelSpec?.display_name || kernelSpec?.name); - } - } - - public async selectJupyterKernel( - resource: Resource, - connection: INotebookProviderConnection | undefined, - type: 'raw' | 'jupyter', - currentKernelDisplayName: string | undefined - ): Promise { - let kernel: KernelSpecInterpreter | undefined; - const settings = this.configService.getSettings(resource); - const isLocalConnection = - connection?.localLaunch ?? - settings.datascience.jupyterServerURI.toLowerCase() === Settings.JupyterServerLocalLaunch; - - if (isLocalConnection) { - kernel = await this.selectLocalJupyterKernel(resource, connection?.type || type, currentKernelDisplayName); - } else if (connection && connection.type === 'jupyter') { - kernel = await this.selectRemoteJupyterKernel(resource, connection, currentKernelDisplayName); - } - return kernel; - } - - 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 { - let selection: KernelSpecInterpreter = {}; - if (notebookMetadata?.kernelspec) { - selection.kernelSpec = await this.kernelService.findMatchingKernelSpec( - notebookMetadata?.kernelspec, - sessionManager, - cancelToken - ); - if (selection.kernelSpec) { - selection.interpreter = await this.kernelService.findMatchingInterpreter( - selection.kernelSpec, - cancelToken - ); - sendTelemetryEvent(Telemetry.UseExistingKernel); - - // Make sure we update the environment in the kernel before using it - await this.kernelService.updateKernelEnvironment( - selection.interpreter, - selection.kernelSpec, - cancelToken - ); - } else if (!cancelToken?.isCancellationRequested) { - // No kernel info, hence prmopt to use current interpreter as a kernel. - const activeInterpreter = await this.interpreterService.getActiveInterpreter(resource); - if (activeInterpreter) { - selection = await this.useInterpreterAsKernel( - resource, - activeInterpreter, - 'jupyter', - notebookMetadata.kernelspec.display_name, - sessionManager, - disableUI, - cancelToken - ); - } else { - telemetryProps.promptedToSelect = true; - selection = await 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) { - selection.interpreter = activeInterpreter; - selection.kernelSpec = await this.kernelService.searchAndRegisterKernel( - activeInterpreter, - disableUI, - cancelToken - ); - } - } - - return selection; - } - - // Get our kernelspec and interpreter for a local raw connection - private async getKernelForLocalRawConnection( - resource: Resource, - notebookMetadata?: nbformat.INotebookMetadata, - cancelToken?: CancellationToken - ): Promise { - const selection: KernelSpecInterpreter = {}; - - // First use our kernel finder to locate a kernelspec on disk - selection.kernelSpec = await this.kernelFinder.findKernelSpec( - resource, - notebookMetadata?.kernelspec, - cancelToken - ); - - if (selection.kernelSpec) { - // Locate the interpreter that matches our kernelspec - selection.interpreter = await this.kernelService.findMatchingInterpreter(selection.kernelSpec, cancelToken); - } - return selection; - } - - 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 {}; - } - // Check if ipykernel is installed in this kernel. - if (selection.selection.interpreter && type === 'jupyter') { - sendTelemetryEvent(Telemetry.SwitchToInterpreterAsKernel); - return this.useInterpreterAsKernel( - resource, - selection.selection.interpreter, - type, - undefined, - session, - false, - cancelToken - ); - } else if (selection.selection.interpreter && type === 'raw') { - return this.useInterpreterAndDefaultKernel(selection.selection.interpreter); - } else if (selection.selection.kernelModel) { - sendTelemetryEvent(Telemetry.SwitchToExistingKernel, undefined, { - language: this.computeLanguage(selection.selection.kernelModel.language) - }); - // tslint:disable-next-line: no-any - const interpreter = selection.selection.kernelModel - ? await this.kernelService.findMatchingInterpreter(selection.selection.kernelModel, cancelToken) - : undefined; - return { - kernelSpec: selection.selection.kernelSpec, - interpreter, - kernelModel: selection.selection.kernelModel - }; - } else if (selection.selection.kernelSpec) { - sendTelemetryEvent(Telemetry.SwitchToExistingKernel, undefined, { - language: this.computeLanguage(selection.selection.kernelSpec.language) - }); - const interpreter = selection.selection.kernelSpec - ? await this.kernelService.findMatchingInterpreter(selection.selection.kernelSpec, cancelToken) - : undefined; - await this.kernelService.updateKernelEnvironment(interpreter, selection.selection.kernelSpec, cancelToken); - return { kernelSpec: selection.selection.kernelSpec, interpreter }; - } else { - return {}; - } - } - - // When switching to an interpreter in raw kernel mode then just create a default kernelspec for that interpreter to use - private async useInterpreterAndDefaultKernel(interpreter: PythonInterpreter): Promise { - const kernelSpec = createDefaultKernelSpec(interpreter.displayName); - return { kernelSpec, interpreter }; - } - - /** - * 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 - * @param {Resource} resource - * @param {PythonInterpreter} interpreter - * @param type - * @param {string} [displayNameOfKernelNotFound] - * @param {IJupyterSessionManager} [session] - * @param {boolean} [disableUI] - * @param {CancellationToken} [cancelToken] - * @returns {Promise} - * @memberof KernelSelector - */ - private async useInterpreterAsKernel( - resource: Resource, - interpreter: PythonInterpreter, - type: 'raw' | 'jupyter' | 'noConnection', - displayNameOfKernelNotFound?: string, - session?: IJupyterSessionManager, - disableUI?: boolean, - cancelToken?: CancellationToken - ): Promise { - let kernelSpec: IJupyterKernelSpec | undefined; - - if (await this.kernelDepdencyService.areDependenciesInstalled(interpreter, cancelToken)) { - // Find the kernel associated with this interpter. - 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.fallbackToUseActiveInterpeterAsKernel().format( - displayNameOfKernelNotFound - ) - ) - .then(noop, noop); - } - - sendTelemetryEvent(Telemetry.UseInterpreterAsKernel); - return { 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(); - - return { 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 deleted file mode 100644 index 2ca9674edc3d..000000000000 --- a/src/client/datascience/jupyter/kernels/kernelService.ts +++ /dev/null @@ -1,558 +0,0 @@ -// 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 { PythonInterpreter } 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 | PythonInterpreter)} item - * @returns {item is PythonInterpreter} - */ -function isInterpreter(item: nbformat.IKernelspecMetadata | PythonInterpreter): item is PythonInterpreter { - // Interpreters will not have a `display_name` property, but have `path` and `type` properties. - return ( - !!(item as PythonInterpreter).path && - !!(item as PythonInterpreter).type && - !(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 {PythonInterpreter} 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: PythonInterpreter, - sessionManager?: IJupyterSessionManager | undefined, - cancelToken?: CancellationToken - ): Promise; - public async findMatchingKernelSpec( - option: nbformat.IKernelspecMetadata | PythonInterpreter, - 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: PythonInterpreter, - 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 {PythonInterpreter} 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: PythonInterpreter, - 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: PythonInterpreter | 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 {PythonInterpreter} interpreter - * @memberof KernelService - */ - private generateKernelNameForIntepreter(interpreter: PythonInterpreter): 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 deleted file mode 100644 index 093a31fe5e2a..000000000000 --- a/src/client/datascience/jupyter/kernels/kernelSwitcher.ts +++ /dev/null @@ -1,97 +0,0 @@ -// 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 { KernelSelector, KernelSpecInterpreter } from './kernelSelector'; - -@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: KernelSpecInterpreter): 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.kernelSpec || kernel.kernelModel - ); - if (potential && Object.keys(potential).length > 0) { - kernel = potential; - continue; - } - } - throw ex; - } - } - } - private async switchToKernel(notebook: INotebook, kernel: KernelSpecInterpreter): Promise { - if (notebook.connection?.type === 'raw' && kernel.interpreter) { - const response = await this.kernelDependencyService.installMissingDependencies(kernel.interpreter); - if (response === KernelInterpreterDependencyResponse.cancel) { - return; - } - } - - const switchKernel = async (newKernel: KernelSpecInterpreter) => { - // Change the kernel. A status update should fire that changes our display - await notebook.setKernelSpec( - newKernel.kernelSpec || newKernel.kernelModel!, - this.configService.getSettings(notebook.resource).datascience.jupyterLaunchTimeout, - newKernel.interpreter - ); - }; - - const kernelDisplayName = kernel.kernelSpec?.display_name || kernel.kernelModel?.display_name; - const kernelName = kernel.kernelSpec?.name || kernel.kernelModel?.name; - // One of them is bound to be non-empty. - const displayName = kernelDisplayName || kernelName || ''; - const options: ProgressOptions = { - location: ProgressLocation.Notification, - cancellable: false, - title: DataScience.switchingKernelProgress().format(displayName) - }; - await this.appShell.withProgress(options, async (_, __) => switchKernel(kernel!)); - } -} diff --git a/src/client/datascience/jupyter/kernels/types.ts b/src/client/datascience/jupyter/kernels/types.ts deleted file mode 100644 index 6134cb8b09ab..000000000000 --- a/src/client/datascience/jupyter/kernels/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import type { Session } from '@jupyterlab/services'; -import { CancellationToken, QuickPickItem } from 'vscode'; -import { Resource } from '../../../common/types'; -import { PythonInterpreter } from '../../../pythonEnvironments/info'; -import { IJupyterKernel, IJupyterKernelSpec } from '../../types'; - -export type LiveKernelModel = IJupyterKernel & Partial & { session: Session.IModel }; - -export interface IKernelSpecQuickPickItem extends QuickPickItem { - /** - * Whether a - * - Kernel spec (IJupyterKernelSpec) - * - Active kernel (IJupyterKernel) or - * - Interpreter has been selected. - * If interpreter is selected, then we might need to install this as a kernel to get the kernel spec. - * - * @type {({ kernelModel: IJupyterKernel; kernelSpec: IJupyterKernelSpec; interpreter: undefined } - * | { kernelModel: undefined; kernelSpec: IJupyterKernelSpec; interpreter: undefined } - * | { kernelModel: undefined; kernelSpec: undefined; interpreter: PythonInterpreter })} - * @memberof IKernelSpecQuickPickItem - */ - selection: - | { kernelModel: LiveKernelModel; kernelSpec: undefined; interpreter: undefined } - | { kernelModel: undefined; kernelSpec: IJupyterKernelSpec; interpreter: undefined } - | { kernelModel: undefined; kernelSpec: undefined; interpreter: PythonInterpreter }; -} - -export interface IKernelSelectionListProvider { - getKernelSelections(resource: Resource, cancelToken?: CancellationToken): Promise; -} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts deleted file mode 100644 index e106da37113d..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts +++ /dev/null @@ -1,159 +0,0 @@ -// 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 { PythonInterpreter } 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 deleted file mode 100644 index 23b736386922..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts +++ /dev/null @@ -1,350 +0,0 @@ -// 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 { PythonInterpreter } from '../../../pythonEnvironments/info'; -import { LiveShare, LiveShareCommands } from '../../constants'; -import { - ICell, - IJupyterKernelSpec, - INotebook, - INotebookCompletion, - INotebookExecutionInfo, - INotebookExecutionLogger, - INotebookProviderConnection, - InterruptResult, - KernelSocketInformation -} from '../../types'; -import { LiveKernelModel } 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 onKernelChanged: Event = new EventEmitter< - IJupyterKernelSpec | LiveKernelModel - >().event; - public onKernelRestarted = new EventEmitter().event; - public onKernelInterrupted = new EventEmitter().event; - public onDisposed = new EventEmitter().event; - 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(): PythonInterpreter | undefined { - return; - } - - public getKernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined { - return; - } - - public setKernelSpec(_spec: IJupyterKernelSpec | LiveKernelModel, _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 deleted file mode 100644 index 9e0c9a19c82f..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts +++ /dev/null @@ -1,158 +0,0 @@ -// 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 deleted file mode 100644 index b6c7b496e23c..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts +++ /dev/null @@ -1,66 +0,0 @@ -// 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 { LiveKernelModel } 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( - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - cancelToken?: CancellationToken - ): Promise { - return this.realSessionManager.startNew(kernelSpec, 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 deleted file mode 100644 index bac9057a4ec4..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManagerFactory.ts +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index e0cecc2a4e84..000000000000 --- a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts +++ /dev/null @@ -1,199 +0,0 @@ -// 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 - }; - } - } - }; - - 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 deleted file mode 100644 index 316ed151c639..000000000000 --- a/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts +++ /dev/null @@ -1,409 +0,0 @@ -// 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 deleted file mode 100644 index 5dea42d8dded..000000000000 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -// tslint:disable-next-line: no-require-imports -import cloneDeep = require('lodash/cloneDeep'); -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 { PythonInterpreter } from '../../../pythonEnvironments/info'; -import { Identifiers, LiveShare, LiveShareCommands, RegExpValues } from '../../constants'; -import { - IDataScienceFileSystem, - IJupyterSession, - IJupyterSessionManager, - IJupyterSessionManagerFactory, - INotebook, - INotebookExecutionLogger, - INotebookMetadataLive, - INotebookServer, - INotebookServerLaunchInfo -} from '../../types'; -import { JupyterServerBase } from '../jupyterServer'; -import { KernelSelector } from '../kernels/kernelSelector'; -import { HostJupyterNotebook } from './hostJupyterNotebook'; -import { LiveShareParticipantHost } from './liveShareParticipantMixin'; -import { IRoleBasedObject } from './roleBasedFactory'; - -// tslint:disable-next-line: no-require-imports -// 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 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.kernelSpec) { - await possibleSession.changeKernel( - info.kernelSpec, - this.configService.getSettings(resource).datascience.jupyterLaunchTimeout - ); - } - - // Start a session (or use the existing one) - const session = possibleSession || (await sessionManager.startNew(info.kernelSpec, 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.interpreter?.displayName - ) { - const kernelInfo = await (launchInfo.connectionInfo.localLaunch - ? this.kernelSelector.getKernelForLocalConnection( - resource, - 'jupyter', - sessionManager, - notebookMetadata, - isTestExecution(), - cancelToken - ) - : this.kernelSelector.getKernelForRemoteConnection( - resource, - sessionManager, - notebookMetadata, - cancelToken - )); - - const kernelInfoToUse = kernelInfo?.kernelSpec || kernelInfo?.kernelModel; - if (kernelInfoToUse) { - launchInfo.kernelSpec = kernelInfoToUse; - - // For the interpreter, make sure to select the one matching the kernel. - launchInfo.interpreter = kernelInfoToUse.metadata?.interpreter?.path - ? (cloneDeep(kernelInfoToUse.metadata.interpreter) as PythonInterpreter) - : 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 deleted file mode 100644 index 84a1ac362b32..000000000000 --- a/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts +++ /dev/null @@ -1,159 +0,0 @@ -// 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 deleted file mode 100644 index 86de298eaad4..000000000000 --- a/src/client/datascience/jupyter/liveshare/responseQueue.ts +++ /dev/null @@ -1,90 +0,0 @@ -// 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 deleted file mode 100644 index c85f6a645018..000000000000 --- a/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts +++ /dev/null @@ -1,117 +0,0 @@ -// 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 deleted file mode 100644 index a06f6c828ac0..000000000000 --- a/src/client/datascience/jupyter/liveshare/serverCache.ts +++ /dev/null @@ -1,150 +0,0 @@ -// 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 deleted file mode 100644 index 5e662b135171..000000000000 --- a/src/client/datascience/jupyter/liveshare/types.ts +++ /dev/null @@ -1,52 +0,0 @@ -// 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 deleted file mode 100644 index 9c6f39c10a31..000000000000 --- a/src/client/datascience/jupyter/liveshare/utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -// 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 deleted file mode 100644 index b520e60d75b5..000000000000 --- a/src/client/datascience/jupyter/notebookStarter.ts +++ /dev/null @@ -1,291 +0,0 @@ -// 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[], - 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); - - // 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, - 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 - ): Promise { - // Parallelize as much as possible. - const promisedArgs: Promise[] = []; - promisedArgs.push(Promise.resolve('--no-browser')); - promisedArgs.push(this.getNotebookDirArgument(tempDirPromise)); - 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 - ): Promise { - if (!customCommandLine || customCommandLine.length === 0) { - return this.generateDefaultArguments(useDefaultConfig, tempDirPromise); - } - return this.generateCustomArguments(customCommandLine); - } - - /** - * Gets the `--notebook-dir` argument. - * - * @private - * @param {Promise} tempDirectory - * @returns {Promise} - * @memberof NotebookStarter - */ - private async getNotebookDirArgument(tempDirectory: Promise): Promise { - const tempDir = await tempDirectory; - return `--notebook-dir=${tempDir.path}`; - } - - /** - * 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 deleted file mode 100644 index 8ab1206c594d..000000000000 --- a/src/client/datascience/jupyter/oldJupyterVariables.ts +++ /dev/null @@ -1,453 +0,0 @@ -// 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'; - -// 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 - let language = PYTHON_LANGUAGE; - if (notebook) { - const kernel = notebook.getKernelSpec(); - language = kernel && kernel.language ? kernel.language : 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 && - notebook.getKernelSpec()?.language === 'python' && - 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 deleted file mode 100644 index 481af8e0bfee..000000000000 --- a/src/client/datascience/jupyter/serverPreload.ts +++ /dev/null @@ -1,91 +0,0 @@ -// 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 deleted file mode 100644 index 6bcca160975d..000000000000 --- a/src/client/datascience/jupyter/serverSelector.ts +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -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; -} - -@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(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: uriItem.uri, - detail: DataScience.jupyterSelectURIMRUDetail().format(uriDate.toLocaleString()), - newChoice: false - }); - } - }); - - return items; - } -} diff --git a/src/client/datascience/jupyter/variableScriptLoader.ts b/src/client/datascience/jupyter/variableScriptLoader.ts deleted file mode 100644 index 8e75093f820f..000000000000 --- a/src/client/datascience/jupyter/variableScriptLoader.ts +++ /dev/null @@ -1,69 +0,0 @@ -// 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 deleted file mode 100644 index ca774a2c041f..000000000000 --- a/src/client/datascience/jupyterDebugService.ts +++ /dev/null @@ -1,415 +0,0 @@ -// 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 deleted file mode 100644 index 9e01b206dece..000000000000 --- a/src/client/datascience/jupyterUriProviderRegistration.ts +++ /dev/null @@ -1,107 +0,0 @@ -// 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 deleted file mode 100644 index 7c1019bb62ac..000000000000 --- a/src/client/datascience/jupyterUriProviderWrapper.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index d2bf2034d9a1..000000000000 --- a/src/client/datascience/kernel-launcher/helpers.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { IInterpreterService } from '../../interpreter/contracts'; -import { PythonInterpreter } 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 deleted file mode 100644 index a2e52732b250..000000000000 --- a/src/client/datascience/kernel-launcher/kernelDaemon.ts +++ /dev/null @@ -1,138 +0,0 @@ -// 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 (options.cwd) { - throw new Error("'cwd' 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 deleted file mode 100644 index cba5c6d1d093..000000000000 --- a/src/client/datascience/kernel-launcher/kernelDaemonPool.ts +++ /dev/null @@ -1,182 +0,0 @@ -// 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 } 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 { PythonInterpreter } 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?: PythonInterpreter - ): 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) => 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) => 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) => d.preWarm()).catch(traceError.bind(`Failed to prewarm kernel daemon ${interpreter.path}`)); - 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) => 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 deleted file mode 100644 index 1e19f400a4ff..000000000000 --- a/src/client/datascience/kernel-launcher/kernelDaemonPreWarmer.ts +++ /dev/null @@ -1,72 +0,0 @@ -// 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 deleted file mode 100644 index 46a3c33c022a..000000000000 --- a/src/client/datascience/kernel-launcher/kernelFinder.ts +++ /dev/null @@ -1,437 +0,0 @@ -// 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 { createDefaultKernelSpec, 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 - ): Promise { - this.cache = 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(this.cache).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 ? result : await this.getDefaultKernelSpec(resource); - } else { - // For a previous default kernel spec, just use it again - foundKernel = this.reuseExistingDefaultSpec(kernelSpecMetadata); - } - } else { - // If we don't have kernel metadata then just get a default spec to use - foundKernel = await this.getDefaultKernelSpec(resource); - } - - this.writeCache(this.cache).ignoreErrors(); - - // Verify that ipykernel is installed into the given kernelspec interpreter - return 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(this.cache).ignoreErrors(); - - // ! as the has and set above verify that we have a return here - return this.workspaceToKernels.get(workspaceFolderId)!; - } - - private reuseExistingDefaultSpec(kernelMetadata: nbformat.IKernelspecMetadata): IJupyterKernelSpec { - return createDefaultKernelSpec(kernelMetadata.display_name); - } - - 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 getDefaultKernelSpec(resource: Resource): Promise { - const activeInterpreter = await this.interpreterService.getActiveInterpreter(resource); - - return createDefaultKernelSpec(activeInterpreter?.displayName); - } - - private async readCache(): Promise { - try { - return JSON.parse( - await this.fs.readLocalFile(path.join(this.context.globalStoragePath, cacheFile)) - ) as string[]; - } catch { - traceInfo('No kernelSpec cache found.'); - return []; - } - } - - private updateCache(newPath: string) { - if (!this.cache.includes(newPath)) { - this.cache.push(newPath); - this.cacheDirty = true; - } - } - - private async writeCache(cache: string[]) { - if (this.cacheDirty) { - await this.fs.writeLocalFile(path.join(this.context.globalStoragePath, cacheFile), JSON.stringify(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 deleted file mode 100644 index a684a749604e..000000000000 --- a/src/client/datascience/kernel-launcher/kernelLauncher.ts +++ /dev/null @@ -1,79 +0,0 @@ -// 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry } from '../../telemetry'; -import { Telemetry } from '../constants'; -import { IDataScienceFileSystem, IJupyterKernelSpec } 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( - kernelSpec: IJupyterKernelSpec, - resource: Resource, - interpreter?: PythonInterpreter - ): Promise { - const connection = await this.getKernelConnection(); - const kernelProcess = new KernelProcess( - this.processExecutionFactory, - this.daemonPool, - connection, - kernelSpec, - this.fs, - resource, - interpreter - ); - await kernelProcess.launch(); - 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 deleted file mode 100644 index 09955c7ef402..000000000000 --- a/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { ChildProcess } from 'child_process'; -import { inject, injectable } from 'inversify'; -import { IDisposable } from 'monaco-editor'; -import { IPythonExecutionService, ObservableExecutionResult } from '../../common/process/types'; -import { Resource } from '../../common/types'; -import { noop } from '../../common/utils/misc'; -import { PythonInterpreter } 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, - kernelSpec: IJupyterKernelSpec, - interpreter?: PythonInterpreter - ): Promise<{ observableOutput: ObservableExecutionResult; daemon: IPythonKernelDaemon | undefined }> { - const daemon = await this.daemonPool.get(resource, kernelSpec, interpreter); - - // 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 (!daemon.start) { - // If we don't have a KernelDaemon here then we have an execution service and should use that to launch - // Typing is a bit funk here, as createDaemon can return an execution service instead of the requested - // daemon class - // tslint:disable-next-line:no-any - const executionService = (daemon as any) as IPythonExecutionService; - - const observableOutput = executionService.execModuleObservable(moduleName, moduleArgs, { env }); - - 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 }); - 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 deleted file mode 100644 index ce7270ffbf19..000000000000 --- a/src/client/datascience/kernel-launcher/kernelProcess.ts +++ /dev/null @@ -1,260 +0,0 @@ -// 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 { PYTHON_LANGUAGE } from '../../common/constants'; -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 { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry } from '../../telemetry'; -import { Telemetry } from '../constants'; -import { cleanEnvironment, findIndexOfConnectionFile } from '../jupyter/kernels/helpers'; -import { IDataScienceFileSystem, IJupyterKernelSpec } from '../types'; -import { PythonKernelLauncherDaemon } from './kernelLauncherDaemon'; -import { IKernelConnection, IKernelProcess, IPythonKernelDaemon, PythonKernelDiedError } from './types'; - -import { KernelDaemonPool } from './kernelDaemonPool'; - -// 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 kernelSpec(): Readonly { - return this.originalKernelSpec; - } - public get connection(): Readonly { - return this._connection; - } - private get isPythonKernel(): boolean { - return this.kernelSpec.language.toLowerCase() === PYTHON_LANGUAGE.toLowerCase(); - } - private _process?: ChildProcess; - private exitEvent = new EventEmitter<{ exitCode?: number; reason?: string }>(); - private pythonKernelLauncher?: PythonKernelLauncherDaemon; - private launchedOnce?: boolean; - private disposed?: boolean; - private kernelDaemon?: IPythonKernelDaemon; - private readonly _kernelSpec: IJupyterKernelSpec; - private readonly originalKernelSpec: IJupyterKernelSpec; - private connectionFile?: string; - constructor( - private readonly processExecutionFactory: IProcessServiceFactory, - private readonly daemonPool: KernelDaemonPool, - private readonly _connection: IKernelConnection, - kernelSpec: IJupyterKernelSpec, - private readonly fs: IDataScienceFileSystem, - private readonly resource: Resource, - private readonly interpreter?: PythonInterpreter - ) { - this.originalKernelSpec = kernelSpec; - this._kernelSpec = cleanEnvironment(kernelSpec); - } - public async interrupt(): Promise { - if (this.kernelDaemon) { - await this.kernelDaemon?.interrupt(); - } - } - - @captureTelemetry(Telemetry.RawKernelProcessLaunch, undefined, true) - public async launch(): 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(); - - 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.'); - } - } - - // 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._kernelSpec); - if (indexOfConnectionFile === -1) { - throw new Error(`Connection file not found in kernelspec json args, ${this._kernelSpec.argv.join(' ')}`); - } - if ( - this.isPythonKernel && - indexOfConnectionFile === 0 && - this._kernelSpec.argv[indexOfConnectionFile - 1] !== '-f' - ) { - throw new Error(`Connection file not found in kernelspec json args, ${this._kernelSpec.argv.join(' ')}`); - } - - // Python kernels are special. Handle the extra arguments. - if (this.isPythonKernel) { - // Slice out -f and the connection file from the args - this._kernelSpec.argv.splice(indexOfConnectionFile - 1, 2); - - // Add in our connection command line args - this._kernelSpec.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._kernelSpec.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() { - let exeObs: ObservableExecutionResult | undefined; - if (this.isPythonKernel) { - this.pythonKernelLauncher = new PythonKernelLauncherDaemon(this.daemonPool); - const kernelDaemonLaunch = await this.pythonKernelLauncher.launch( - this.resource, - this._kernelSpec, - this.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._kernelSpec.argv[0]; - const executionService = await this.processExecutionFactory.create(this.resource); - exeObs = executionService.execObservable(executable, this._kernelSpec.argv.slice(1), { - env: this._kernelSpec.env - }); - } - - 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 deleted file mode 100644 index 2fedfff8f5a6..000000000000 --- a/src/client/datascience/kernel-launcher/types.ts +++ /dev/null @@ -1,85 +0,0 @@ -// 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -import { IJupyterKernelSpec } from '../types'; - -export const IKernelLauncher = Symbol('IKernelLauncher'); -export interface IKernelLauncher { - launch( - kernelSpec: IJupyterKernelSpec, - resource: Resource, - interpreter?: PythonInterpreter - ): 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 kernelSpec: 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 - ): 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 deleted file mode 100644 index ca87b32723f4..000000000000 --- a/src/client/datascience/kernelSocketWrapper.ts +++ /dev/null @@ -1,117 +0,0 @@ -// 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 deleted file mode 100644 index c7ecb30ec1d8..000000000000 --- a/src/client/datascience/liveshare/liveshare.ts +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index 4a4b2f10b5d3..000000000000 --- a/src/client/datascience/liveshare/liveshareProxy.ts +++ /dev/null @@ -1,182 +0,0 @@ -// 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 deleted file mode 100644 index ac255edd586b..000000000000 --- a/src/client/datascience/liveshare/postOffice.ts +++ /dev/null @@ -1,284 +0,0 @@ -// 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 deleted file mode 100644 index 31d94cb26433..000000000000 --- a/src/client/datascience/liveshare/serviceProxy.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index c7e7ca14dcf8..000000000000 --- a/src/client/datascience/messages.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index fcd8fcd4620f..000000000000 --- a/src/client/datascience/monacoMessages.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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 deleted file mode 100644 index f4801a0d4c06..000000000000 --- a/src/client/datascience/multiplexingDebugService.ts +++ /dev/null @@ -1,188 +0,0 @@ -// 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 deleted file mode 100644 index 4cf273010809..000000000000 --- a/src/client/datascience/notebook/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 1c1974832a12..000000000000 --- a/src/client/datascience/notebook/contentProvider.ts +++ /dev/null @@ -1,112 +0,0 @@ -// 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 { 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)); - - 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/executionService.ts b/src/client/datascience/notebook/executionService.ts deleted file mode 100644 index e2d69545c516..000000000000 --- a/src/client/datascience/notebook/executionService.ts +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { nbformat } from '@jupyterlab/coreutils'; -import { inject, injectable } from 'inversify'; -import { Subscription } from 'rxjs'; -import { CancellationToken, CancellationTokenSource } from 'vscode'; -import type { NotebookCell, NotebookCellRunState, NotebookDocument } from 'vscode-proposed'; -import { ICommandManager } from '../../common/application/types'; -import { wrapCancellationTokens } from '../../common/cancellation'; -import '../../common/extensions'; -import { IDisposable } from '../../common/types'; -import { createDeferred } from '../../common/utils/async'; -import { noop } from '../../common/utils/misc'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { Commands, Telemetry, VSCodeNativeTelemetry } from '../constants'; -import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; -import { VSCodeNotebookModel } from '../notebookStorage/vscNotebookModel'; -import { IDataScienceErrorHandler, INotebook, INotebookEditorProvider, INotebookProvider } from '../types'; -import { - handleUpdateDisplayDataMessage, - hasTransientOutputForAnotherCell, - updateCellExecutionCount, - updateCellOutput, - updateCellWithErrorStatus -} from './helpers/executionHelpers'; -import { - clearCellForExecution, - getCellStatusMessageBasedOnFirstCellErrorOutput, - updateCellExecutionTimes -} from './helpers/helpers'; -import { NotebookEditor } from './notebookEditor'; -import { INotebookContentProvider, INotebookExecutionService } from './types'; -// tslint:disable-next-line: no-var-requires no-require-imports -const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); - -/** - * VSC will use this class to execute cells in a notebook. - * This is where we hookup Jupyter with a Notebook in VSCode. - */ -@injectable() -export class NotebookExecutionService implements INotebookExecutionService { - private readonly registeredIOPubListeners = new WeakSet(); - private _notebookProvider?: INotebookProvider; - private readonly pendingExecutionCancellations = new Map(); - private readonly tokensInterrupted = new WeakSet(); - private sentExecuteCellTelemetry: boolean = false; - private get notebookProvider(): INotebookProvider { - this._notebookProvider = - this._notebookProvider || this.serviceContainer.get(INotebookProvider); - return this._notebookProvider!; - } - constructor( - @inject(INotebookStorageProvider) private readonly notebookStorage: INotebookStorageProvider, - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDataScienceErrorHandler) private readonly errorHandler: IDataScienceErrorHandler, - @inject(INotebookContentProvider) private readonly contentProvider: INotebookContentProvider, - @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider - ) {} - @captureTelemetry(Telemetry.ExecuteNativeCell, undefined, true) - public async executeCell(document: NotebookDocument, cell: NotebookCell, token: CancellationToken): Promise { - // Cannot execute empty cells. - if (cell.document.getText().trim().length === 0) { - return; - } - const stopWatch = new StopWatch(); - const notebookAndModel = this.getNotebookAndModel(document); - - // Mark cells as busy (this way there's immediate feedback to users). - // If it does not complete, then restore old state. - const oldCellState = cell.metadata.runState; - cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Running; - - // If we cancel running cells, then restore the state to previous values unless cell has completed. - token.onCancellationRequested(() => { - if (cell.metadata.runState === vscodeNotebookEnums.NotebookCellRunState.Running) { - cell.metadata.runState = oldCellState; - } - }); - - await this.executeIndividualCell(notebookAndModel, document, cell, token, stopWatch); - } - @captureTelemetry(Telemetry.ExecuteNativeCell, undefined, true) - @captureTelemetry(VSCodeNativeTelemetry.RunAllCells, undefined, true) - public async executeAllCells(document: NotebookDocument, token: CancellationToken): Promise { - const stopWatch = new StopWatch(); - const notebookAndModel = this.getNotebookAndModel(document); - document.metadata.runState = vscodeNotebookEnums.NotebookRunState.Running; - // Mark all cells as busy (this way there's immediate feedback to users). - // If it does not complete, then restore old state. - const oldCellStates = new WeakMap(); - document.cells.forEach((cell) => { - if ( - cell.document.getText().trim().length === 0 || - cell.cellKind === vscodeNotebookEnums.CellKind.Markdown - ) { - return; - } - oldCellStates.set(cell, cell.metadata.runState); - cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Running; - }); - - const restoreOldCellState = (cell: NotebookCell) => { - if ( - oldCellStates.has(cell) && - cell.metadata.runState === vscodeNotebookEnums.NotebookCellRunState.Running - ) { - cell.metadata.runState = oldCellStates.get(cell); - } - }; - // If we cancel running cells, then restore the state to previous values unless cell has completed. - token.onCancellationRequested(() => { - document.metadata.runState = vscodeNotebookEnums.NotebookRunState.Idle; - document.cells.forEach(restoreOldCellState); - }); - - let executingAPreviousCellHasFailed = false; - await document.cells.reduce((previousPromise, cellToExecute) => { - return previousPromise.then((previousCellState) => { - // If a previous cell has failed or execution cancelled, the get out. - if ( - executingAPreviousCellHasFailed || - token.isCancellationRequested || - previousCellState === vscodeNotebookEnums.NotebookCellRunState.Error - ) { - executingAPreviousCellHasFailed = true; - restoreOldCellState(cellToExecute); - return; - } - if ( - cellToExecute.document.getText().trim().length === 0 || - cellToExecute.cellKind === vscodeNotebookEnums.CellKind.Markdown - ) { - return; - } - return this.executeIndividualCell(notebookAndModel, document, cellToExecute, token, stopWatch); - }); - }, Promise.resolve(undefined)); - - document.metadata.runState = vscodeNotebookEnums.NotebookRunState.Idle; - } - public cancelPendingExecutions(document: NotebookDocument): void { - this.pendingExecutionCancellations.get(document.uri.fsPath)?.forEach((cancellation) => cancellation.cancel()); // NOSONAR - } - private async getNotebookAndModel( - document: NotebookDocument - ): Promise<{ model: VSCodeNotebookModel; nb: INotebook }> { - const model = await this.notebookStorage.getOrCreateModel(document.uri, undefined, undefined, true); - const nb = await this.notebookProvider.getOrCreateNotebook({ - identity: document.uri, - resource: document.uri, - metadata: model.metadata, - disableUI: false, - getOnly: false - }); - if (!nb) { - throw new Error('Unable to get Notebook object to run cell'); - } - if (!(model instanceof VSCodeNotebookModel)) { - throw new Error('Notebook Model is not of type VSCodeNotebookModel'); - } - return { model, nb }; - } - private sendPerceivedCellExecute(runningStopWatch: StopWatch) { - const props = { notebook: true }; - if (!this.sentExecuteCellTelemetry) { - this.sentExecuteCellTelemetry = true; - sendTelemetryEvent(Telemetry.ExecuteCellPerceivedCold, runningStopWatch.elapsedTime, props); - } else { - sendTelemetryEvent(Telemetry.ExecuteCellPerceivedWarm, runningStopWatch.elapsedTime, props); - } - } - - private async executeIndividualCell( - notebookAndModel: Promise<{ model: VSCodeNotebookModel; nb: INotebook }>, - document: NotebookDocument, - cell: NotebookCell, - token: CancellationToken, - stopWatch: StopWatch - ): Promise { - if (token.isCancellationRequested) { - return; - } - - const { model, nb } = await notebookAndModel; - if (token.isCancellationRequested) { - return; - } - - const editor = this.editorProvider.editors.find((e) => e.model === model); - if (!editor) { - throw new Error('No editor for Model'); - } - if (editor && !(editor instanceof NotebookEditor)) { - throw new Error('Executing Notebook with another Editor'); - } - // If we need to cancel this execution (from our code, due to kernel restarts or similar, then cancel). - const cancelExecution = new CancellationTokenSource(); - if (!this.pendingExecutionCancellations.has(document.uri.fsPath)) { - this.pendingExecutionCancellations.set(document.uri.fsPath, []); - } - // If kernel is restarted while executing, then abort execution. - const cancelExecutionCancellation = new CancellationTokenSource(); - this.pendingExecutionCancellations.get(document.uri.fsPath)?.push(cancelExecutionCancellation); // NOSONAR - - // Replace token with a wrapped cancellation, which will wrap cancellation due to restarts. - const wrappedToken = wrapCancellationTokens(token, cancelExecutionCancellation.token, cancelExecution.token); - const disposable = nb?.onKernelRestarted(() => { - cancelExecutionCancellation.cancel(); - disposable.dispose(); - }); - - // tslint:disable-next-line: no-suspicious-comment - // TODO: How can nb be null? - // We should throw an exception or change return type to be non-nullable. - // Else in places where it shouldn't be null we'd end up treating it as null (i.e. ignoring error conditions, like this). - - this.handleDisplayDataMessages(model, document, nb); - - const deferred = createDeferred(); - wrappedToken.onCancellationRequested(() => { - if (deferred.completed) { - return; - } - - // Interrupt kernel only if original cancellation was cancelled. - // I.e. interrupt kernel only if user attempts to stop the execution by clicking stop button. - if (token.isCancellationRequested && !this.tokensInterrupted.has(token)) { - this.tokensInterrupted.add(token); - this.commandManager.executeCommand(Commands.NotebookEditorInterruptKernel).then(noop, noop); - } - }); - - // Ensure we clear the cell state and trigger a change. - clearCellForExecution(cell); - const executionStopWatch = new StopWatch(); - cell.metadata.runStartTime = new Date().getTime(); - this.contentProvider.notifyChangesToDocument(document); - - let subscription: Subscription | undefined; - let modelClearedEventHandler: IDisposable | undefined; - try { - nb.clear(cell.uri.toString()); // NOSONAR - editor.notifyExecution(cell.document.getText()); - await nb.setLaunchingFile(model.file.path); - const observable = nb.executeObservable( - cell.document.getText(), - document.fileName, - 0, - cell.uri.toString(), - false - ); - subscription = observable?.subscribe( - (cells) => { - if (!modelClearedEventHandler) { - modelClearedEventHandler = model.changed((e) => { - if (e.kind === 'clear') { - // If cell output has been cleared, then clear the output in the observed executable cell. - // Else if user clears output while executing a cell, we add it back. - cells.forEach((c) => (c.data.outputs = [])); - } - }); - } - const rawCellOutput = cells - .filter((item) => item.id === cell.uri.toString()) - .flatMap((item) => (item.data.outputs as unknown) as nbformat.IOutput[]) - .filter((output) => !hasTransientOutputForAnotherCell(output)); - - // Set execution count, all messages should have it - if ( - cells.length && - 'execution_count' in cells[0].data && - typeof cells[0].data.execution_count === 'number' - ) { - const executionCount = cells[0].data.execution_count as number; - if (updateCellExecutionCount(cell, executionCount)) { - this.contentProvider.notifyChangesToDocument(document); - } - } - - if (updateCellOutput(cell, rawCellOutput)) { - this.contentProvider.notifyChangesToDocument(document); - } - }, - (error: Partial) => { - updateCellWithErrorStatus(cell, error); - this.contentProvider.notifyChangesToDocument(document); - this.errorHandler.handleError((error as unknown) as Error).ignoreErrors(); - deferred.resolve(cell.metadata.runState); - }, - () => { - cell.metadata.lastRunDuration = executionStopWatch.elapsedTime; - cell.metadata.runState = wrappedToken.isCancellationRequested - ? vscodeNotebookEnums.NotebookCellRunState.Idle - : vscodeNotebookEnums.NotebookCellRunState.Success; - cell.metadata.statusMessage = ''; - updateCellExecutionTimes(cell, { - startTime: cell.metadata.runStartTime, - duration: cell.metadata.lastRunDuration - }); - - // If there are any errors in the cell, then change status to error. - if (cell.outputs.some((output) => output.outputKind === vscodeNotebookEnums.CellOutputKind.Error)) { - cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Error; - cell.metadata.statusMessage = getCellStatusMessageBasedOnFirstCellErrorOutput(cell.outputs); - } - - this.contentProvider.notifyChangesToDocument(document); - deferred.resolve(cell.metadata.runState); - } - ); - await deferred.promise; - } catch (ex) { - updateCellWithErrorStatus(cell, ex); - this.contentProvider.notifyChangesToDocument(document); - this.errorHandler.handleError(ex).ignoreErrors(); - } finally { - this.sendPerceivedCellExecute(stopWatch); - modelClearedEventHandler?.dispose(); // NOSONAR - subscription?.unsubscribe(); // NOSONAR - // Ensure we remove the cancellation. - const cancellations = this.pendingExecutionCancellations.get(document.uri.fsPath); - const index = cancellations?.indexOf(cancelExecutionCancellation) ?? -1; - if (cancellations && index >= 0) { - cancellations.splice(index, 1); - } - } - return cell.metadata.runState; - } - /** - * Ensure we handle display data messages that can result in updates to other cells. - */ - private handleDisplayDataMessages(model: VSCodeNotebookModel, document: NotebookDocument, nb?: INotebook) { - if (nb && !this.registeredIOPubListeners.has(nb)) { - this.registeredIOPubListeners.add(nb); - //tslint:disable-next-line:no-require-imports - const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); - nb.registerIOPubListener((msg) => { - if ( - jupyterLab.KernelMessage.isUpdateDisplayDataMsg(msg) && - handleUpdateDisplayDataMessage(msg, model, document) - ) { - this.contentProvider.notifyChangesToDocument(document); - } - }); - } - } -} diff --git a/src/client/datascience/notebook/helpers/executionHelpers.ts b/src/client/datascience/notebook/helpers/executionHelpers.ts deleted file mode 100644 index 375b0bbf3bfc..000000000000 --- a/src/client/datascience/notebook/helpers/executionHelpers.ts +++ /dev/null @@ -1,119 +0,0 @@ -// 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 { NotebookCell, NotebookCellRunState, NotebookDocument } from 'vscode'; -import { createErrorOutput } from '../../../../datascience-ui/common/cellFactory'; -import { VSCodeNotebookModel } from '../../notebookStorage/vscNotebookModel'; -import { createVSCCellOutputsFromOutputs, translateErrorOutput } from './helpers'; - -export interface IBaseCellVSCodeMetadata { - end_execution_time?: string; - start_execution_time?: string; -} - -export function hasTransientOutputForAnotherCell(output?: nbformat.IOutput) { - return ( - output && - // tslint:disable-next-line: no-any - (output as any).output_type === 'display_data' && - // tslint:disable-next-line: no-any - 'transient' in (output as any) && - // tslint:disable-next-line: no-any - Object.keys((output as any).transient).length > 0 - ); -} - -/** - * 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, - model: VSCodeNotebookModel, - document: NotebookDocument -): boolean { - // Find any cells that have this same display_id - return ( - model.cells.filter((cellToCheck, index) => { - if (cellToCheck.data.cell_type !== 'code') { - return false; - } - - let updated = false; - const data: nbformat.ICodeCell = cellToCheck.data as nbformat.ICodeCell; - const changedOutputs = data.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) { - 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 && JSON.stringify(vscCell.outputs) === JSON.stringify(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 deleted file mode 100644 index ef79b129aae1..000000000000 --- a/src/client/datascience/notebook/helpers/helpers.ts +++ /dev/null @@ -1,591 +0,0 @@ -// 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 { - concatMultilineStringInput, - concatMultilineStringOutput, - 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'); - -// This is the custom type we are adding into nbformat.IBaseCellMetadata -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; - } -} - -/** - * Converts a NotebookModel into VSCode friendly format. - */ -export function notebookModelToVSCNotebookData(model: INotebookModel): NotebookData { - const cells = model.cells - .map(createVSCNotebookCellDataFromCell.bind(undefined, model)) - .filter((item) => !!item) - .map((item) => item!); - - const defaultLanguage = getDefaultCodeLanguage(model); - return { - cells, - languages: [defaultLanguage], - metadata: { - 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 { - if (vscCell.cellKind === vscodeNotebookEnums.CellKind.Markdown) { - const data = createMarkdownCellFromVSCNotebookCell(vscCell); - return { - data, - file: model.file.toString(), - id: uuid(), - line: 0, - state: CellState.init - }; - } else if (vscCell.language === 'raw') { - const data = createRawCellFromVSCNotebookCell(vscCell); - return { - data, - file: model.file.toString(), - id: uuid(), - line: 0, - state: CellState.init - }; - } else { - const data = createCodeCellFromVSCNotebookCell(vscCell); - return { - data, - file: model.file.toString(), - id: uuid(), - line: 0, - state: CellState.init - }; - } -} - -/** - * 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: concatMultilineStringInput(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: concatMultilineStringInput(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: concatMultilineStringInput(cell.data.source), - outputs - }; -} - -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); - if (fn) { - return 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.`); - return { - outputKind: vscodeNotebookEnums.CellOutputKind.Rich, - // tslint:disable-next-line: no-any - data: output.data as any, - metadata: { custom: { vscode: { outputType: output.output_type } } } - }; - } -} - -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 (outputType === 'execute_result') { - metadata.custom.vscode.execution_count = (output as nbformat.IExecuteResult).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']: concatMultilineStringOutput(output.text) - }, - 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; -} -function translateCellDisplayOutput( - output: CellDisplayOutput -): - | nbformat.IStream - | nbformat.IDisplayData - | nbformat.IDisplayUpdate - | nbformat.IExecuteResult - | nbformat.IUnrecognizedOutput { - const outputType: nbformat.OutputType = output.metadata?.custom?.vscode?.outputType; - switch (outputType) { - case 'stream': { - return { - output_type: 'stream', - name: output.metadata?.custom?.vscode?.name, - text: splitMultilineString(output.data['text/plain']) - }; - } - case 'display_data': { - const metadata = getSanitizedCellMetadata(output.metadata?.custom); - return { - output_type: 'display_data', - data: output.data, - metadata - }; - } - case 'execute_result': { - const metadata = getSanitizedCellMetadata(output.metadata?.custom); - return { - output_type: 'execute_result', - data: output.data, - metadata, - execution_count: output.metadata?.custom?.vscode?.execution_count - }; - } - case 'update_display_data': { - const metadata = getSanitizedCellMetadata(output.metadata?.custom); - return { - output_type: 'update_display_data', - data: output.data, - metadata - }; - } - 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; - } - return unknownOutput; - } - } -} - -/** - * 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/integration.ts b/src/client/datascience/notebook/integration.ts deleted file mode 100644 index de2ad1d8b43d..000000000000 --- a/src/client/datascience/notebook/integration.ts +++ /dev/null @@ -1,159 +0,0 @@ -// 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 { NotebookKernel } from './notebookKernel'; -import { NotebookOutputRenderer } from './renderer'; -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(NotebookKernel) private readonly notebookKernel: NotebookKernel, - @inject(NotebookOutputRenderer) private readonly renderer: NotebookOutputRenderer, - @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) - ); - this.disposables.push( - this.vscNotebook.registerNotebookKernel(JupyterNotebookView, ['**/*.ipynb'], this.notebookKernel) - ); - this.disposables.push( - this.vscNotebook.registerNotebookOutputRenderer( - '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' - ] - }, - this.renderer - ) - ); - } 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/notebookDisposeService.ts b/src/client/datascience/notebook/notebookDisposeService.ts deleted file mode 100644 index 6bc470618aeb..000000000000 --- a/src/client/datascience/notebook/notebookDisposeService.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 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 { 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 - ) {} - public async activate(): Promise { - if (this.env.channel !== 'insiders') { - return; - } - - this.vscNotebook.onDidCloseNotebookDocument(this.onDidCloseNotebookDocument, this, this.disposables); - } - private onDidCloseNotebookDocument(document: NotebookDocument) { - this.notebookProvider.disposeAssociatedNotebook({ identity: document.uri }); - } -} diff --git a/src/client/datascience/notebook/notebookEditor.ts b/src/client/datascience/notebook/notebookEditor.ts deleted file mode 100644 index 00f2c23f231d..000000000000 --- a/src/client/datascience/notebook/notebookEditor.ts +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { CellKind, 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 { - INotebook, - INotebookEditor, - INotebookModel, - INotebookProvider, - InterruptResult, - IStatusProvider -} from '../types'; -import { getDefaultCodeLanguage } from './helpers/helpers'; -import { INotebookExecutionService } from './types'; - -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 executionService: INotebookExecutionService, - private readonly commandManager: ICommandManager, - private readonly notebookProvider: INotebookProvider, - 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, CellKind.Code, [], undefined); - for (let i = totalLength - 1; i >= 0; i = i - 1) { - editor.delete(i); - } - }); - } - public notifyExecution(code: string) { - this._executed.fire(this); - this.executedCode.fire(code); - } - public async interruptKernel(): Promise { - this.executionService.cancelPendingExecutions(this.document); - - if (this.restartingKernel) { - return; - } - const notebook = await this.notebookProvider.getOrCreateNotebook({ - resource: this.file, - identity: this.file, - getOnly: true - }); - if (!notebook || this.restartingKernel) { - return; - } - const status = this.statusProvider.set(DataScience.interruptKernelStatus(), true, undefined, undefined); - - try { - const interruptTimeout = this.configurationService.getSettings(this.file).datascience - .jupyterInterruptTimeout; - const result = await notebook.interruptKernel(interruptTimeout); - 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 { - this.executionService.cancelPendingExecutions(this.document); - - sendTelemetryEvent(Telemetry.RestartKernelCommand); - if (this.restartingKernel) { - return; - } - const notebook = await this.notebookProvider.getOrCreateNotebook({ - resource: this.file, - identity: this.file, - getOnly: true - }); - - if (notebook && !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(notebook); - } else if (response === yes) { - await this.restartKernelInternal(notebook); - } - } else { - await this.restartKernelInternal(notebook); - } - } - } - public dispose() { - this._closed.fire(this); - } - private async restartKernelInternal(notebook: INotebook): Promise { - this.restartingKernel = true; - - // Set our status - const status = this.statusProvider.set(DataScience.restartingKernelStatus(), true, undefined, undefined); - - // Disable running cells. - const [cellRunnable, runnable] = [this.document.metadata.cellRunnable, this.document.metadata.runnable]; - try { - this.document.metadata.cellRunnable = false; - this.document.metadata.runnable = false; - await notebook.restartKernel( - this.configurationService.getSettings(this.file).datascience.jupyterInterruptTimeout - ); - - // // Compute if dark or not. - // const knownDark = await this.isDark(); - - // // Before we run any cells, update the dark setting - // await 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 && 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; - // Restore previous state. - [this.document.metadata.cellRunnable, this.document.metadata.runnable] = [cellRunnable, runnable]; - } - } - 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 deleted file mode 100644 index 4ac70ab6823d..000000000000 --- a/src/client/datascience/notebook/notebookEditorCompatibilitySupport.ts +++ /dev/null @@ -1,87 +0,0 @@ -// 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 deleted file mode 100644 index 618ec2275c94..000000000000 --- a/src/client/datascience/notebook/notebookEditorProvider.ts +++ /dev/null @@ -1,226 +0,0 @@ -// 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 { 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'; -import { INotebookExecutionService } from './types'; - -/** - * 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 executionService = this.serviceContainer.get(INotebookExecutionService); - editor = new NotebookEditor( - model, - doc, - this.vscodeNotebook, - executionService, - this.commandManager, - notebookProvider, - 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 deleted file mode 100644 index 6889d7305327..000000000000 --- a/src/client/datascience/notebook/notebookEditorProviderWrapper.ts +++ /dev/null @@ -1,144 +0,0 @@ -// 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/notebookKernel.ts b/src/client/datascience/notebook/notebookKernel.ts deleted file mode 100644 index cbb6a34a60ff..000000000000 --- a/src/client/datascience/notebook/notebookKernel.ts +++ /dev/null @@ -1,91 +0,0 @@ -// 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 { NotebookCell, NotebookDocument, NotebookKernel as VSCNotebookKernel } from 'vscode-proposed'; -import { noop } from '../../common/utils/misc'; -import { INotebookExecutionService } from './types'; - -/** - * Cancellation token source that can be cancelled multiple times. - */ -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(); - } -} - -/** - * VSC will use this class to execute cells in a notebook. - * This is where we hookup Jupyter with a Notebook in VSCode. - */ -@injectable() -export class NotebookKernel implements VSCNotebookKernel { - get preloads(): Uri[] { - return []; - } - public get label(): string { - return 'Jupyter'; - } - private cellExecutions = new WeakMap(); - private documentExecutions = new WeakMap(); - constructor(@inject(INotebookExecutionService) private readonly execution: INotebookExecutionService) {} - public executeCell(document: NotebookDocument, cell: NotebookCell) { - if (this.cellExecutions.has(cell)) { - return; - } - const source = new MultiCancellationTokenSource(); - this.cellExecutions.set(cell, source); - this.execution - .executeCell(document, cell, source.token) - .finally(() => { - if (this.cellExecutions.get(cell) === source) { - this.cellExecutions.delete(cell); - } - }) - .catch(noop); - } - public executeAllCells(document: NotebookDocument) { - if (this.documentExecutions.has(document)) { - return; - } - const source = new MultiCancellationTokenSource(); - this.documentExecutions.set(document, source); - this.execution - .executeAllCells(document, source.token) - .finally(() => { - if (this.documentExecutions.get(document) === source) { - this.documentExecutions.delete(document); - } - }) - .catch(noop); - } - public cancelCellExecution(_document: NotebookDocument, cell: NotebookCell) { - this.cellExecutions.get(cell)?.cancel(); // NOSONAR - } - public cancelAllCellsExecution(document: NotebookDocument) { - this.documentExecutions.get(document)?.cancel(); // NOSONAR - } -} diff --git a/src/client/datascience/notebook/renderer.ts b/src/client/datascience/notebook/renderer.ts deleted file mode 100644 index 61083ea24574..000000000000 --- a/src/client/datascience/notebook/renderer.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { injectable } from 'inversify'; -import * as path from 'path'; -import { CellOutputKind, NotebookOutputRenderer as VSCNotebookOutputRenderer, Uri } from 'vscode'; -import { NotebookRenderRequest } from 'vscode-proposed'; -import { EXTENSION_ROOT_DIR } from '../../constants'; -import { JupyterNotebookRenderer } from './constants'; - -@injectable() -export class NotebookOutputRenderer implements VSCNotebookOutputRenderer { - public readonly preloads: Uri[] = []; - constructor() { - const renderersFolder = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'renderers'); - this.preloads = [Uri.file(path.join(renderersFolder, 'renderers.js'))]; - } - - // @ts-ignore - public render(document: NotebookDocument, request: NotebookRenderRequest) { - let outputToSend = request.output; - if (request.output.outputKind === CellOutputKind.Rich && request.mimeType in request.output.data) { - // tslint:disable-next-line: no-any - const metadata: Record = {}; - // 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; - } - } - outputToSend = { - ...request.output, - // Send only what we need & ignore other mimeTypes. - data: { - [request.mimeType]: request.output.data[request.mimeType] - }, - metadata - }; - } - return ` - - `; - } -} diff --git a/src/client/datascience/notebook/rendererExtension.ts b/src/client/datascience/notebook/rendererExtension.ts deleted file mode 100644 index 0488cb9aa68d..000000000000 --- a/src/client/datascience/notebook/rendererExtension.ts +++ /dev/null @@ -1,41 +0,0 @@ -// 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 deleted file mode 100644 index d3956a71f7db..000000000000 --- a/src/client/datascience/notebook/rendererExtensionDownloader.ts +++ /dev/null @@ -1,58 +0,0 @@ -// 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 deleted file mode 100644 index f1d5943113a9..000000000000 --- a/src/client/datascience/notebook/serviceRegistry.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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 { NotebookContentProvider } from './contentProvider'; -import { NotebookExecutionService } from './executionService'; -import { NotebookIntegration } from './integration'; -import { NotebookDisposeService } from './notebookDisposeService'; -import { NotebookKernel } from './notebookKernel'; -import { NotebookOutputRenderer } from './renderer'; -import { NotebookSurveyBanner, NotebookSurveyDataLogger } from './survey'; -import { INotebookContentProvider, INotebookExecutionService } from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(INotebookContentProvider, NotebookContentProvider); - serviceManager.addSingleton(INotebookExecutionService, NotebookExecutionService); - serviceManager.addSingleton( - IExtensionSingleActivationService, - NotebookIntegration - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - NotebookDisposeService - ); - serviceManager.addSingleton(NotebookIntegration, NotebookIntegration); - serviceManager.addSingleton(NotebookKernel, NotebookKernel); - serviceManager.addSingleton(NotebookOutputRenderer, NotebookOutputRenderer); - serviceManager.addSingleton(NotebookSurveyBanner, NotebookSurveyBanner); - serviceManager.addSingleton( - IExtensionSingleActivationService, - NotebookSurveyDataLogger - ); -} diff --git a/src/client/datascience/notebook/survey.ts b/src/client/datascience/notebook/survey.ts deleted file mode 100644 index aabac9de00b6..000000000000 --- a/src/client/datascience/notebook/survey.ts +++ /dev/null @@ -1,196 +0,0 @@ -// 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 deleted file mode 100644 index 639306ab54d1..000000000000 --- a/src/client/datascience/notebook/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import type { CancellationToken } from 'vscode'; -import type { - NotebookCell, - NotebookContentProvider as VSCodeNotebookContentProvider, - NotebookDocument -} from 'vscode-proposed'; - -export const INotebookExecutionService = Symbol('INotebookExecutionService'); -export interface INotebookExecutionService { - cancelPendingExecutions(document: NotebookDocument): void; - executeCell(document: NotebookDocument, cell: NotebookCell, token: CancellationToken): Promise; - executeAllCells(document: NotebookDocument, token: CancellationToken): Promise; -} - -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 deleted file mode 100644 index b888b7312be4..000000000000 --- a/src/client/datascience/notebookAndInteractiveTracker.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index fc38a4b05cf1..000000000000 --- a/src/client/datascience/notebookStorage/baseModel.ts +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; -import { Event, EventEmitter, Memento, Uri } from 'vscode'; -import { ICryptoUtils } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { pruneCell } from '../common'; -import { NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; -import { LiveKernelModel } from '../jupyter/kernels/types'; -import { ICell, IJupyterKernelSpec, INotebookMetadataLive, INotebookModel } from '../types'; -import { isUntitled } from './nativeEditorStorage'; - -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; -}; - -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 isUntitled(this); - } - 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 - } - : this.notebookJson.metadata; - } - 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.interpreter, change.kernelSpec); - 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( - interpreter: PythonInterpreter | undefined, - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined - ): boolean { - let changed = false; - // Get our kernel_info and language_info from the current notebook - if ( - interpreter && - interpreter.version && - this.notebookJson.metadata && - this.notebookJson.metadata.language_info && - this.notebookJson.metadata.language_info.version !== interpreter.version.raw - ) { - this.notebookJson.metadata.language_info.version = interpreter.version.raw; - changed = true; - } else if (!interpreter && this.notebookJson.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 - this.notebookJson.metadata.language_info = undefined; - changed = true; - } - - if (kernelSpec && this.notebookJson.metadata && !this.notebookJson.metadata.kernelspec) { - // Add a new spec in this case - this.notebookJson.metadata.kernelspec = { - name: kernelSpec.name || kernelSpec.display_name || '', - display_name: kernelSpec.display_name || kernelSpec.name || '' - }; - this.kernelId = kernelSpec.id; - changed = true; - } else if (kernelSpec && this.notebookJson.metadata && this.notebookJson.metadata.kernelspec) { - // Spec exists, just update name and display_name - const name = kernelSpec.name || kernelSpec.display_name || ''; - const displayName = kernelSpec.display_name || kernelSpec.name || ''; - if ( - this.notebookJson.metadata.kernelspec.name !== name || - this.notebookJson.metadata.kernelspec.display_name !== displayName || - this.kernelId !== kernelSpec.id - ) { - changed = true; - this.notebookJson.metadata.kernelspec.name = name; - this.notebookJson.metadata.kernelspec.display_name = displayName; - this.kernelId = kernelSpec.id; - } - } - - // Update our kernel id in our global storage too - this.setStoredKernelId(kernelSpec?.id); - - return changed; - } - - private ensureNotebookJson() { - if (!this.notebookJson || !this.notebookJson.metadata) { - // const pythonNumber = await this.extractPythonMainVersion(this._state.notebookJson); - const pythonNumber = this.pythonNumber; - // 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. - this.notebookJson = { - metadata: metadata, - nbformat: 4, - nbformat_minor: 2 - }; - } - } - - 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 deleted file mode 100644 index b440b40709f9..000000000000 --- a/src/client/datascience/notebookStorage/factory.ts +++ /dev/null @@ -1,55 +0,0 @@ -// 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 deleted file mode 100644 index 5a22462ee0b4..000000000000 --- a/src/client/datascience/notebookStorage/nativeEditorProvider.ts +++ /dev/null @@ -1,338 +0,0 @@ -// 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, - IWebPanelProvider, - 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 - ) { - 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); - // 1 second timeout on save so don't wait. Just write and forget - this.storage.save(model, cancellation).ignoreErrors(); - } - public async saveCustomDocumentAs(document: CustomDocument, targetResource: Uri): Promise { - const model = await this.loadModel(document.uri); - // 1 second timeout on save so don't wait. Just write and forget - this.storage.saveAs(model, targetResource).ignoreErrors(); - } - public async revertCustomDocument(document: CustomDocument, cancellation: CancellationToken): Promise { - const model = await this.loadModel(document.uri); - // 1 second time limit on this so don't wait. - this.storage.revert(model, cancellation).ignoreErrors(); - } - 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 { - // 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. - const 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(IWebPanelProvider), - 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) - const result = this.createNotebookEditor(model, 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; - } 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, title); - } -} diff --git a/src/client/datascience/notebookStorage/nativeEditorStorage.ts b/src/client/datascience/notebookStorage/nativeEditorStorage.ts deleted file mode 100644 index 7076f8330f28..000000000000 --- a/src/client/datascience/notebookStorage/nativeEditorStorage.ts +++ /dev/null @@ -1,498 +0,0 @@ -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'; - -const KeyPrefix = 'notebook-storage-'; -const NotebookTransferKey = 'notebook-transfered'; - -export function isUntitled(model?: INotebookModel): boolean { - return isUntitledFile(model?.file); -} - -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 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(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(filePath); - } - - private async writeToStorage(filePath: string, contents?: string, cancelToken?: CancellationToken): Promise { - try { - if (!cancelToken?.isCancellationRequested) { - if (contents) { - await this.fs.createLocalDirectory(path.dirname(filePath)); - if (!cancelToken?.isCancellationRequested) { - 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 - }; - }); - - // 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(); - } - } - - 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 { - try { - // Use this to read from the extension global location - const contents = await this.fs.readLocalFile(file.fsPath); - 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 deleted file mode 100644 index 1c99712aa943..000000000000 --- a/src/client/datascience/notebookStorage/notebookModel.ts +++ /dev/null @@ -1,261 +0,0 @@ -// 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 { concatMultilineStringInput, 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 = concatMultilineStringInput(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 deleted file mode 100644 index 9fbbcc17b281..000000000000 --- a/src/client/datascience/notebookStorage/notebookModelEditEvent.ts +++ /dev/null @@ -1,23 +0,0 @@ -// 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 deleted file mode 100644 index 81b907c33053..000000000000 --- a/src/client/datascience/notebookStorage/notebookStorageProvider.ts +++ /dev/null @@ -1,127 +0,0 @@ -// 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 { 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 models = new Set(); - private readonly disposables: IDisposable[] = []; - constructor( - @inject(INotebookStorage) private readonly storage: INotebookStorage, - @inject(IDisposableRegistry) disposables: IDisposableRegistry - ) { - 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 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); - } - - private getNextNewNotebookUri(forVSCodeNotebooks?: boolean): Uri { - return generateNewNotebookUri(NotebookStorageProvider.untitledCounter, undefined, forVSCodeNotebooks); - } - - private trackModel(model: INotebookModel): INotebookModel { - this.disposables.push(model); - this.models.add(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, - this.disposables - ); - return model; - } -} diff --git a/src/client/datascience/notebookStorage/types.ts b/src/client/datascience/notebookStorage/types.ts deleted file mode 100644 index f26f2c5aafe0..000000000000 --- a/src/client/datascience/notebookStorage/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -// 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 deleted file mode 100644 index dae3e3357cbc..000000000000 --- a/src/client/datascience/notebookStorage/vscNotebookModel.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import type { nbformat } from '@jupyterlab/coreutils'; -import * as assert from 'assert'; -import { Memento, Uri } from 'vscode'; -import { NotebookDocument } from '../../../../types/vscode-proposed'; -import { splitMultilineString } from '../../../datascience-ui/common'; -import { traceError } from '../../common/logger'; -import { ICryptoUtils } from '../../common/types'; -import { NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; -import { createCellFromVSCNotebookCell, updateVSCNotebookAfterTrustingNotebook } from '../notebook/helpers/helpers'; -import { ICell } from '../types'; -import { BaseNotebookModel } from './baseModel'; - -// This is the custom type we are adding into nbformat.IBaseCellMetadata -interface IBaseCellVSCodeMetadata { - end_execution_time?: string; - start_execution_time?: string; -} - -// 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[] { - return this.document - ? this.document.cells.map((cell) => createCellFromVSCNotebookCell(cell, this)) - : this._cells; - } - private document?: NotebookDocument; - - constructor( - isTrusted: boolean, - file: Uri, - cells: ICell[], - globalMemento: Memento, - crypto: ICryptoUtils, - json: Partial = {}, - indentAmount: string = ' ', - pythonNumber: number = 3 - ) { - 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 = []; - } - } - public updateCellSource(cellId: string, source: string): void { - const cell = this.getCell(cellId); - if (cell) { - cell.data.source = splitMultilineString(source); - } - } - public clearCellOutput(cell: ICell, clearExecutionCount: boolean): void { - if (cell.data.cell_type === 'code' && clearExecutionCount) { - cell.data.execution_count = null; - } - if (cell.data.metadata.vscode) { - (cell.data.metadata.vscode as IBaseCellVSCodeMetadata).start_execution_time = undefined; - (cell.data.metadata.vscode as IBaseCellVSCodeMetadata).end_execution_time = undefined; - } - cell.data.outputs = []; - // We want to trigger change events. - this.update({ - source: 'user', - kind: 'clear', - oldDirty: this.isDirty, - newDirty: true, - oldCells: [cell] - }); - } - /** - * @param {number} start The zero-based location in the array after which the new item is to be added. - */ - public addCell(cell: ICell, start: number): void { - this._cells.splice(start, 0, cell); - // Get model to fire events. - this.update({ - source: 'user', - kind: 'insert', - cell: cell, - index: start, - oldDirty: this.isDirty, - newDirty: true - }); - } - public deleteCell(cell: ICell): void { - const index = this._cells.indexOf(cell); - this._cells.splice(index, 1); - // Get model to fire events. - this.update({ - source: 'user', - kind: 'remove', - cell: cell, - index: index, - oldDirty: this.isDirty, - newDirty: true - }); - } - public swapCells(cellToSwap: ICell, cellToSwapWith: ICell) { - assert.notEqual(cellToSwap, cellToSwapWith, 'Cannot swap cell with the same cell'); - - const indexOfCellToSwap = this.cells.indexOf(cellToSwap); - const indexOfCellToSwapWith = this.cells.indexOf(cellToSwapWith); - this._cells[indexOfCellToSwap] = cellToSwapWith; - this._cells[indexOfCellToSwapWith] = cellToSwap; - // Get model to fire events. - this.update({ - source: 'user', - kind: 'swap', - firstCellId: cellToSwap.id, - secondCellId: cellToSwapWith.id, - oldDirty: this.isDirty, - newDirty: true - }); - } - public updateCellOutput(cell: ICell, outputs: nbformat.IOutput[]) { - cell.data.outputs = outputs; - } - public updateCellExecutionCount(cell: ICell, executionCount: number) { - cell.data.execution_count = executionCount; - } - public updateCellMetadata(cell: ICell, metadata: Partial) { - const originalVscodeMetadata: IBaseCellVSCodeMetadata = - (cell.data.metadata.vscode as IBaseCellVSCodeMetadata) || {}; - // Update our model with the new metadata stored in jupyter. - cell.data.metadata = { - ...cell.data.metadata, - vscode: { - ...originalVscodeMetadata, - ...metadata - } - // This line is required because ts-node sucks on GHA. - // tslint:disable-next-line: no-any - } as any; - } - protected generateNotebookJson() { - const json = super.generateNotebookJson(); - // 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; - } - private getCell(cellId: string) { - const cell = this.cells.find((item) => item.id === cellId); - if (!cell) { - traceError( - `Syncing Cell Editor aborted, Unable to find corresponding ICell for ${cellId}`, - new Error('ICell not found') - ); - return; - } - return cell; - } -} diff --git a/src/client/datascience/plotting/plotViewer.ts b/src/client/datascience/plotting/plotViewer.ts deleted file mode 100644 index 0cb84362eeef..000000000000 --- a/src/client/datascience/plotting/plotViewer.ts +++ /dev/null @@ -1,181 +0,0 @@ -// 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, IWebPanelProvider, 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 { WebViewHost } from '../webViewHost'; -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 WebViewHost implements IPlotViewer, IDisposable { - private closedEvent: EventEmitter = new EventEmitter(); - private removedEvent: EventEmitter = new EventEmitter(); - - constructor( - @inject(IWebPanelProvider) provider: IWebPanelProvider, - @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 deleted file mode 100644 index b77944f17029..000000000000 --- a/src/client/datascience/plotting/plotViewerMessageListener.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { IWebPanel, IWebPanelMessageListener } 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 IWebPanelMessageListener { - private disposedCallback: () => void; - private callback: (message: string, payload: any) => void; - private viewChanged: (panel: IWebPanel) => void; - - constructor( - callback: (message: string, payload: any) => void, - viewChanged: (panel: IWebPanel) => 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: IWebPanel) { - // 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 deleted file mode 100644 index 57cd21195976..000000000000 --- a/src/client/datascience/plotting/plotViewerProvider.ts +++ /dev/null @@ -1,66 +0,0 @@ -// 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 deleted file mode 100644 index d10491da37e5..000000000000 --- a/src/client/datascience/plotting/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index 9eaad2da74d2..000000000000 --- a/src/client/datascience/preWarmVariables.ts +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 5f0215d08736..000000000000 --- a/src/client/datascience/progress/decorator.ts +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index 89abfaecaf6f..000000000000 --- a/src/client/datascience/progress/messages.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index 87ccadf9f450..000000000000 --- a/src/client/datascience/progress/progressReporter.ts +++ /dev/null @@ -1,106 +0,0 @@ -// 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 deleted file mode 100644 index 12f51a14f121..000000000000 --- a/src/client/datascience/progress/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index 61c1fe758b07..000000000000 --- a/src/client/datascience/raw-kernel/liveshare/guestRawNotebookProvider.ts +++ /dev/null @@ -1,139 +0,0 @@ -// 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 deleted file mode 100644 index 3efa172d4cd5..000000000000 --- a/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts +++ /dev/null @@ -1,239 +0,0 @@ -// 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 { KernelSelector, KernelSpecInterpreter } from '../../jupyter/kernels/kernelSelector'; -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 rawSession = new RawJupyterSession(this.kernelLauncher, resource, this.outputChannel, noop, noop); - 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 kernelSpecInterpreter = await this.kernelSelector.getKernelForLocalConnection( - resource, - 'raw', - undefined, - notebookMetadata, - disableUI, - cancelToken - ); - - // Interpreter is optional, but we must have a kernel spec for a raw launch - if (!kernelSpecInterpreter.kernelSpec) { - notebookPromise.reject('Failed to find a kernelspec to use for ipykernel launch'); - } else { - await rawSession.connect( - kernelSpecInterpreter.kernelSpec, - launchTimeout, - kernelSpecInterpreter.interpreter, - cancelToken - ); - - // Get the execution info for our notebook - const info = await this.getExecutionInfo(kernelSpecInterpreter); - - 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(kernelSpecInterpreter: KernelSpecInterpreter): Promise { - return { - connectionInfo: this.getConnection(), - uri: Settings.JupyterServerLocalLaunch, - interpreter: kernelSpecInterpreter.interpreter, - kernelSpec: kernelSpecInterpreter.kernelSpec, - 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 deleted file mode 100644 index 8ed7fcb67d11..000000000000 --- a/src/client/datascience/raw-kernel/rawJupyterSession.ts +++ /dev/null @@ -1,241 +0,0 @@ -// 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { BaseJupyterSession } from '../baseJupyterSession'; -import { Identifiers, Telemetry } from '../constants'; -import { LiveKernelModel } 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 { IJupyterKernelSpec, ISessionWithSocket } from '../types'; - -// Error thrown when we are unable to start a raw kernel session -export class RawKernelSessionStartError extends Error { - constructor(kernelTitle: string) { - super(localize.DataScience.rawKernelSessionFailed().format(kernelTitle)); - } -} - -/* -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 - ) { - super(restartSessionUsed); - } - - @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( - kernelSpec: IJupyterKernelSpec, - timeout: number, - interpreter?: PythonInterpreter, - 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(kernelSpec, interpreter, 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(kernelSpec.display_name || kernelSpec.name); - } 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.kernelSpec = newSession.kernelProcess?.kernelSpec; - this.interpreter = interpreter; - - this.outputChannel.appendLine( - localize.DataScience.kernelStarted().format(this.kernelSpec.display_name || this.kernelSpec.name) - ); - } - } 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.kernelSpec; - } - - public async createNewKernelSession( - kernel: IJupyterKernelSpec | LiveKernelModel, - timeoutMS: number, - interpreter?: PythonInterpreter, - _cancelToken?: CancellationToken - ): Promise { - if (!kernel || 'session' in kernel) { - // Don't allow for connecting to a LiveKernelModel - throw new Error(localize.DataScience.sessionDisposed()); - } - - this.outputChannel.appendLine(localize.DataScience.kernelStarted().format(kernel.display_name || kernel.name)); - - const newSession = await waitForPromise(this.startRawSession(kernel, interpreter), timeoutMS); - - if (!newSession) { - throw new RawKernelSessionStartError(kernel.display_name || kernel.name); - } - - 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.kernelSpec, this.session, this.interpreter); - } - } - protected async createRestartSession( - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - _session: ISessionWithSocket, - interpreter?: PythonInterpreter, - cancelToken?: CancellationToken - ): Promise { - if (!kernelSpec || 'session' in kernelSpec) { - // Need to have connected before restarting and can't use a LiveKernelModel - throw new Error(localize.DataScience.sessionDisposed()); - } - const startPromise = this.startRawSession(kernelSpec, interpreter, cancelToken); - return startPromise.then((session) => { - this.restartSessionCreated(session.kernel); - return session; - }); - } - - @captureTelemetry(Telemetry.RawKernelStartRawSession, undefined, true) - private async startRawSession( - kernelSpec: IJupyterKernelSpec, - interpreter?: PythonInterpreter, - cancelToken?: CancellationToken - ): Promise { - const cancellationPromise = createPromiseFromCancellation({ - cancelAction: 'reject', - defaultValue: undefined, - token: cancelToken - }) as Promise; - cancellationPromise.catch(noop); - - const process = await Promise.race([ - this.kernelLauncher.launch(kernelSpec, this.resource, interpreter), - 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 deleted file mode 100644 index e0d7a67fe61f..000000000000 --- a/src/client/datascience/raw-kernel/rawKernel.ts +++ /dev/null @@ -1,274 +0,0 @@ -// 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 { 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: kernelProcess.kernelSpec.name, - 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 deleted file mode 100644 index 559f6cc8bed7..000000000000 --- a/src/client/datascience/raw-kernel/rawNotebookProvider.ts +++ /dev/null @@ -1,149 +0,0 @@ -// 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 deleted file mode 100644 index e37591626fc7..000000000000 --- a/src/client/datascience/raw-kernel/rawNotebookProviderWrapper.ts +++ /dev/null @@ -1,139 +0,0 @@ -// 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 deleted file mode 100644 index 5c9cd416f156..000000000000 --- a/src/client/datascience/raw-kernel/rawNotebookSupportedService.ts +++ /dev/null @@ -1,71 +0,0 @@ -// 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 deleted file mode 100644 index a02965319c4b..000000000000 --- a/src/client/datascience/raw-kernel/rawSession.ts +++ /dev/null @@ -1,156 +0,0 @@ -// 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 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); - // 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.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 { - throw new Error('Not yet implemented'); - } - 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 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 deleted file mode 100644 index c592f35bcade..000000000000 --- a/src/client/datascience/raw-kernel/rawSocket.ts +++ /dev/null @@ -1,237 +0,0 @@ -// 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 deleted file mode 100644 index 4bacce95e0ff..000000000000 --- a/src/client/datascience/serviceRegistry.ts +++ /dev/null @@ -1,323 +0,0 @@ -// 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. - 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 deleted file mode 100644 index 7fadde77a3a6..000000000000 --- a/src/client/datascience/shiftEnterBanner.ts +++ /dev/null @@ -1,133 +0,0 @@ -// 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 deleted file mode 100644 index c82ad4bad9dc..000000000000 --- a/src/client/datascience/statusProvider.ts +++ /dev/null @@ -1,128 +0,0 @@ -// 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 deleted file mode 100644 index c0deeb79e47c..000000000000 --- a/src/client/datascience/themeFinder.ts +++ /dev/null @@ -1,293 +0,0 @@ -// 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 deleted file mode 100644 index e837433a9d3b..000000000000 --- a/src/client/datascience/types.ts +++ /dev/null @@ -1,1403 +0,0 @@ -// 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 { PythonInterpreter } 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 { KernelSpecInterpreter } from './jupyter/kernels/kernelSelector'; -import { LiveKernelModel } 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; - // 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; - /** - * The python interpreter associated with the kernel. - */ - interpreter: PythonInterpreter | undefined; - uri: string | undefined; // Different from the connectionInfo as this is the setting used, not the result - kernelSpec: IJupyterKernelSpec | undefined | LiveKernelModel; - 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; - /** - * The python interpreter associated with the kernel. - * - * @type {(PythonInterpreter | undefined)} - * @memberof INotebookServerLaunchInfo - */ - interpreter: PythonInterpreter | undefined; - uri: string | undefined; // Different from the connectionInfo as this is the setting used, not the result - kernelSpec: IJupyterKernelSpec | undefined | LiveKernelModel; - 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; - 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(): PythonInterpreter | undefined; - getKernelSpec(): IJupyterKernelSpec | LiveKernelModel | undefined; - setKernelSpec( - spec: IJupyterKernelSpec | LiveKernelModel, - timeoutMS: number, - interpreter: PythonInterpreter | undefined - ): 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; - readonly status: ServerStatus; - 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( - kernel: IJupyterKernelSpec | LiveKernelModel, - timeoutMS: number, - interpreter?: PythonInterpreter - ): 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( - kernelSpec: IJupyterKernelSpec | LiveKernelModel | undefined, - 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; - 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; -} - -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: PythonInterpreter, - 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; - 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; kernel: KernelSpecInterpreter }>; - - /** - * 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: KernelSpecInterpreter): 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: PythonInterpreter, - token?: CancellationToken - ): Promise; - areDependenciesInstalled(interpreter: PythonInterpreter, _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 deleted file mode 100644 index f91b905b9f02..000000000000 --- a/src/client/datascience/utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -// 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/webViewHost.ts b/src/client/datascience/webViewHost.ts deleted file mode 100644 index ba4c9c49eeaf..000000000000 --- a/src/client/datascience/webViewHost.ts +++ /dev/null @@ -1,388 +0,0 @@ -// 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, ViewColumn, WebviewPanel, WorkspaceConfiguration } from 'vscode'; - -import { IWebPanel, IWebPanelMessageListener, IWebPanelProvider, IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import { traceInfo } from '../common/logger'; -import { IConfigurationService, IDisposable, Resource } from '../common/types'; -import { createDeferred, Deferred } 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 { DefaultTheme, GatherExtension, Telemetry } from './constants'; -import { CssMessages, IGetCssRequest, IGetMonacoThemeRequest, SharedMessages } from './messages'; -import { ICodeCssGenerator, IDataScienceExtraSettings, IThemeFinder, WebViewViewChangeEventArgs } from './types'; - -@injectable() // For some reason this is necessary to get the class hierarchy to work. -export abstract class WebViewHost implements IDisposable { - protected get isDisposed(): boolean { - return this.disposed; - } - protected viewState: { visible: boolean; active: boolean } = { visible: false, active: false }; - private disposed: boolean = false; - private webPanel: IWebPanel | undefined; - private webPanelInit: Deferred | undefined = createDeferred(); - private messageListener: IWebPanelMessageListener; - private themeIsDarkPromise: Deferred | undefined = createDeferred(); - private startupStopwatch = new StopWatch(); - private readonly _disposables: IDisposable[] = []; - - constructor( - @unmanaged() protected configService: IConfigurationService, - @unmanaged() private provider: IWebPanelProvider, - @unmanaged() private cssGenerator: ICodeCssGenerator, - @unmanaged() protected themeFinder: IThemeFinder, - @unmanaged() protected workspaceService: IWorkspaceService, - @unmanaged() - messageListenerCtor: ( - callback: (message: string, payload: {}) => void, - viewChanged: (panel: IWebPanel) => void, - disposed: () => void - ) => IWebPanelMessageListener, - @unmanaged() private rootPath: string, - @unmanaged() private scripts: string[], - @unmanaged() private _title: string, - @unmanaged() private viewColumn: ViewColumn, - @unmanaged() protected readonly useCustomEditorApi: boolean, - @unmanaged() private readonly enableVariablesDuringDebugging: boolean, - @unmanaged() private readonly hideKernelToolbarInInteractiveWindow: Promise - ) { - // Create our message listener for our web panel. - this.messageListener = messageListenerCtor( - this.onMessage.bind(this), - this.webPanelViewStateChanged.bind(this), - this.dispose.bind(this) - ); - - // 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 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; - } - - this._disposables.forEach((item) => item.dispose()); - - this.webPanelInit = undefined; - this.themeIsDarkPromise = undefined; - } - } - public get title() { - return this._title; - } - - public setTitle(newTitle: string) { - this._title = newTitle; - if (!this.isDisposed && this.webPanel) { - this.webPanel.setTitle(newTitle); - } - } - - public setTheme(isDark: boolean) { - if (this.themeIsDarkPromise && !this.themeIsDarkPromise.resolved) { - this.themeIsDarkPromise.resolve(isDark); - } else { - this.themeIsDarkPromise = createDeferred(); - this.themeIsDarkPromise.resolve(isDark); - } - } - protected asWebviewUri(localResource: Uri) { - if (!this.webPanel) { - throw new Error('asWebViewUri called too early'); - } - return this.webPanel?.asWebviewUri(localResource); - } - - protected abstract get owningResource(): Resource; - - //tslint:disable-next-line:no-any - protected onMessage(message: string, payload: any) { - switch (message) { - case SharedMessages.Started: - this.webPanelRendered(); - break; - - case CssMessages.GetCssRequest: - this.handleCssRequest(payload as IGetCssRequest).ignoreErrors(); - break; - - case CssMessages.GetMonacoThemeRequest: - this.handleMonacoThemeRequest(payload as IGetMonacoThemeRequest).ignoreErrors(); - break; - - default: - break; - } - } - - protected postMessage(type: T, payload?: M[T]): Promise { - // Then send it the message - return this.postMessageInternal(type.toString(), payload); - } - - protected shareMessage(type: T, payload?: M[T]) { - // Send our remote message. - this.messageListener.onMessage(type.toString(), payload); - } - - protected onViewStateChanged(_args: WebViewViewChangeEventArgs) { - noop(); - } - - // tslint:disable-next-line:no-any - protected async postMessageInternal(type: string, payload?: any): Promise { - if (this.webPanelInit) { - // Make sure the webpanel is up before we send it anything. - await this.webPanelInit.promise; - - // Then send it the message - this.webPanel?.postMessage({ type: type.toString(), payload: payload }); - } - } - - 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 isDark(): Promise { - return this.themeIsDarkPromise ? this.themeIsDarkPromise.promise : Promise.resolve(false); - } - - 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.webPanelInit = this.webPanelInit ? this.webPanelInit : 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] : [] - }); - - // 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(); - } - - protected async sendLocStrings() { - const locStrings = isTestExecution() ? '{}' : localize.getCollectionJSON(); - this.postMessageInternal(SharedMessages.LocInit, locStrings).ignoreErrors(); - } - - // Post a message to our webpanel 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(); - }; - - // If our webpanel fails to load then just dispose ourselves - private onWebPanelLoadFailed = async () => { - this.dispose(); - }; - - private getValue(workspaceConfig: WorkspaceConfiguration, section: string, defaultValue: T): T { - if (workspaceConfig) { - return workspaceConfig.get(section, defaultValue); - } - return defaultValue; - } - - private webPanelViewStateChanged = (webPanel: IWebPanel) => { - 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 }); - }; - - @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 }); - } - - // tslint:disable-next-line:no-any - private webPanelRendered() { - if (this.webPanelInit && !this.webPanelInit.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.webPanelInit.resolve(); - - traceInfo('Web view react rendered'); - } - - // On started, resend our init data. - this.sendLocStrings().ignoreErrors(); - this.onDataScienceSettingsChanged().ignoreErrors(); - } - - // 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/debugger/constants.ts b/src/client/debugger/constants.ts index 6d4af08019be..a2ac198a597d 100644 --- a/src/client/debugger/constants.ts +++ b/src/client/debugger/constants.ts @@ -3,8 +3,5 @@ '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'; +export const PythonDebuggerTypeName = 'debugpy'; diff --git a/src/client/debugger/extension/adapter/activator.ts b/src/client/debugger/extension/adapter/activator.ts index be64226946c5..999c00366ed6 100644 --- a/src/client/debugger/extension/adapter/activator.ts +++ b/src/client/debugger/extension/adapter/activator.ts @@ -2,37 +2,52 @@ // Licensed under the MIT License. 'use strict'; - +import { Uri } from 'vscode'; import { inject, injectable } from 'inversify'; import { IExtensionSingleActivationService } from '../../../activation/types'; import { IDebugService } from '../../../common/application/types'; -import { IDisposableRegistry } from '../../../common/types'; +import { IConfigurationService, IDisposableRegistry } from '../../../common/types'; +import { ICommandManager } from '../../../common/application/types'; import { DebuggerTypeName } from '../../constants'; import { IAttachProcessProviderFactory } from '../attachQuickPick/types'; import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from '../types'; @injectable() export class DebugAdapterActivator implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; constructor( @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IConfigurationService) private readonly configSettings: IConfigurationService, + @inject(ICommandManager) private commandManager: ICommandManager, @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 + private readonly attachProcessProviderFactory: IAttachProcessProviderFactory, ) {} public async activate(): Promise { this.attachProcessProviderFactory.registerCommands(); this.disposables.push( - this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory) + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory), + ); + this.disposables.push( + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debuggerPromptFactory), ); + this.disposables.push( - this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debuggerPromptFactory) + this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory), ); this.disposables.push( - this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory) + this.debugService.onDidStartDebugSession((debugSession) => { + if (this.shouldTerminalFocusOnStart(debugSession.workspaceFolder?.uri)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }), ); } + + private shouldTerminalFocusOnStart(uri: Uri | undefined): boolean { + return this.configSettings.getSettings(uri)?.terminal.focusAfterLaunch; + } } diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index 6379874d8e47..edef16368dc0 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -10,26 +10,38 @@ import { DebugAdapterExecutable, DebugAdapterServer, DebugSession, - WorkspaceFolder + l10n, + 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 { traceError, traceLog, traceVerbose } from '../../../logging'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; import { IDebugAdapterDescriptorFactory } from '../types'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { Common, Interpreters } from '../../../common/utils/localize'; +import { IPersistentStateFactory } from '../../../common/types'; +import { Commands } from '../../../common/constants'; +import { ICommandManager } from '../../../common/application/types'; +import { getDebugpyPath } from '../../pythonDebugger'; + +// persistent state names, exported to make use of in testing +export enum debugStateKeys { + doNotShowAgain = 'doNotShowPython36DebugDeprecatedAgain', +} @injectable() export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory { constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IApplicationShell) private readonly appShell: IApplicationShell + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, ) {} + public async createDebugAdapterDescriptor( session: DebugSession, - _executable: DebugAdapterExecutable | undefined + _executable: DebugAdapterExecutable | undefined, ): Promise { const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments; @@ -46,39 +58,43 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac if (configuration.request === 'attach') { if (configuration.connect !== undefined) { + traceLog( + `Connecting to DAP Server at: ${configuration.connect.host ?? '127.0.0.1'}:${ + configuration.connect.port + }`, + ); return new DebugAdapterServer(configuration.connect.port, configuration.connect.host ?? '127.0.0.1'); } else if (configuration.port !== undefined) { + traceLog(`Connecting to DAP Server at: ${configuration.host ?? '127.0.0.1'}:${configuration.port}`); 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); - } + const command = await this.getDebugAdapterPython(configuration, session.workspaceFolder); + if (command.length !== 0) { + const executable = command.shift() ?? 'python'; // "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 args = command.concat([configuration.debugAdapterPath, ...logArgs]); + traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); + return new DebugAdapterExecutable(executable, args); } + const debugpyPath = await getDebugpyPath(); + if (!debugpyPath) { + traceError('Could not find debugpy path.'); + throw new Error('Could not find debugpy path.'); + } + const debuggerAdapterPathToUse = path.join(debugpyPath, 'adapter'); - 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]); + const args = command.concat([debuggerAdapterPathToUse, ...logArgs]); + traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); + return new DebugAdapterExecutable(executable, args); } // Unlikely scenario. @@ -96,28 +112,77 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac * @returns {Promise} Path to the python interpreter for this workspace. * @memberof DebugAdapterDescriptorFactory */ - private async getPythonPath( + private async getDebugAdapterPython( configuration: LaunchRequestArguments | AttachRequestArguments, - workspaceFolder?: WorkspaceFolder - ): Promise { - if (configuration.pythonPath) { - return configuration.pythonPath; + workspaceFolder?: WorkspaceFolder, + ): Promise { + if (configuration.debugAdapterPython !== undefined) { + return this.getExecutableCommand( + await this.interpreterService.getInterpreterDetails(configuration.debugAdapterPython), + ); + } else if (configuration.pythonPath) { + return this.getExecutableCommand( + await this.interpreterService.getInterpreterDetails(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; + return this.getExecutableCommand(interpreter); } - const interpreters = await this.interpreterService.getInterpreters(resourceUri); + await this.interpreterService.hasInterpreters(); // Wait until we know whether we have an interpreter + const interpreters = this.interpreterService.getInterpreters(resourceUri); if (interpreters.length === 0) { this.notifySelectInterpreter().ignoreErrors(); - return ''; + return []; } traceVerbose(`Picking first available interpreter to launch the DA '${interpreters[0].path}'`); - return interpreters[0].path; + return this.getExecutableCommand(interpreters[0]); + } + + private async showDeprecatedPythonMessage() { + const notificationPromptEnabled = this.persistentState.createGlobalPersistentState( + debugStateKeys.doNotShowAgain, + false, + ); + if (notificationPromptEnabled.value) { + return; + } + const prompts = [Interpreters.changePythonInterpreter, Common.doNotShowAgain]; + const selection = await showErrorMessage( + l10n.t('The debugger in the python extension no longer supports python versions minor than 3.7.'), + { modal: true }, + ...prompts, + ); + if (!selection) { + return; + } + if (selection == Interpreters.changePythonInterpreter) { + await this.commandManager.executeCommand(Commands.Set_Interpreter); + } + if (selection == Common.doNotShowAgain) { + // Never show the message again + await this.persistentState + .createGlobalPersistentState(debugStateKeys.doNotShowAgain, false) + .updateValue(true); + } + } + + private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { + if (interpreter) { + if ( + (interpreter.version?.major ?? 0) < 3 || + ((interpreter.version?.major ?? 0) <= 3 && (interpreter.version?.minor ?? 0) <= 6) + ) { + this.showDeprecatedPythonMessage(); + } + return interpreter.path.length > 0 ? [interpreter.path] : []; + } + return []; } /** @@ -129,9 +194,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac * @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.' - ); + await showErrorMessage(l10n.t('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 index 58b2f7ff4358..907b895170c6 100644 --- a/src/client/debugger/extension/adapter/logging.ts +++ b/src/client/debugger/extension/adapter/logging.ts @@ -9,7 +9,7 @@ import { DebugAdapterTrackerFactory, DebugConfiguration, DebugSession, - ProviderResult + ProviderResult, } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; @@ -58,7 +58,7 @@ class DebugSessionLoggingTracker implements DebugAdapterTracker { private log(message: string) { if (this.enabled) { - this.stream!.write(`${this.timer.elapsedTime} ${message}`); + this.stream!.write(`${this.timer.elapsedTime} ${message}`); // NOSONAR } } diff --git a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts index b9aa0d14542b..04117e9838d1 100644 --- a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts +++ b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts @@ -2,34 +2,27 @@ // Licensed under the MIT License. 'use strict'; - -import { inject, injectable } from 'inversify'; +import { 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 { launch } from '../../../common/vscodeApis/browserApis'; +import { showInformationMessage } from '../../../common/vscodeApis/windowApis'; 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 - ) {} + constructor(private promptCheck: IPromptShowState) {} 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'); - } - }); + const prompts = [Common.moreInfo]; + showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts).then((selection) => { + if (selection === prompts[0]) { + launch('https://aka.ms/migrateToDebugpy'); + } + }); } } @@ -71,13 +64,10 @@ class OutdatedDebuggerPromptState implements IPromptShowState { @injectable() export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory { private readonly promptCheck: OutdatedDebuggerPromptState; - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IBrowserService) private browserService: IBrowserService - ) { + constructor() { this.promptCheck = new OutdatedDebuggerPromptState(); } public createDebugAdapterTracker(_session: DebugSession): ProviderResult { - return new OutdatedDebuggerPrompt(this.promptCheck, this.appShell, this.browserService); + return new OutdatedDebuggerPrompt(this.promptCheck); } } diff --git a/src/client/debugger/extension/adapter/remoteLaunchers.ts b/src/client/debugger/extension/adapter/remoteLaunchers.ts index 8f1eab6f9b51..f68f747a8a8c 100644 --- a/src/client/debugger/extension/adapter/remoteLaunchers.ts +++ b/src/client/debugger/extension/adapter/remoteLaunchers.ts @@ -3,24 +3,25 @@ 'use strict'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../common/constants'; import '../../../common/extensions'; +import { getDebugpyPath } from '../../pythonDebugger'; -const pathToPythonLibDir = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); -const pathToDebugger = path.join(pathToPythonLibDir, 'debugpy'); - -export type RemoteDebugOptions = { +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 async function getDebugpyLauncherArgs(options: RemoteDebugOptions, debuggerPath?: string) { + if (!debuggerPath) { + debuggerPath = await getDebugpyPath(); + } -export function getDebugpyPackagePath(): string { - return pathToDebugger; + const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait-for-client'] : []; + return [ + debuggerPath.fileToCommandArgumentForPythonExt(), + '--listen', + `${options.host}:${options.port}`, + ...waitArgs, + ]; } diff --git a/src/client/debugger/extension/attachQuickPick/factory.ts b/src/client/debugger/extension/attachQuickPick/factory.ts index f36c45183965..627962106e88 100644 --- a/src/client/debugger/extension/attachQuickPick/factory.ts +++ b/src/client/debugger/extension/attachQuickPick/factory.ts @@ -20,7 +20,7 @@ export class AttachProcessProviderFactory implements IAttachProcessProviderFacto @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IPlatformService) private readonly platformService: IPlatformService, @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, ) {} public registerCommands() { @@ -29,7 +29,7 @@ export class AttachProcessProviderFactory implements IAttachProcessProviderFacto const disposable = this.commandManager.registerCommand( Commands.PickLocalProcess, () => picker.showQuickPick(), - this + this, ); this.disposableRegistry.push(disposable); } diff --git a/src/client/debugger/extension/attachQuickPick/picker.ts b/src/client/debugger/extension/attachQuickPick/picker.ts index db1d4a8a8a74..a296a9b3163a 100644 --- a/src/client/debugger/extension/attachQuickPick/picker.ts +++ b/src/client/debugger/extension/attachQuickPick/picker.ts @@ -14,7 +14,7 @@ import { IAttachItem, IAttachPicker, IAttachProcessProvider, REFRESH_BUTTON_ICON export class AttachPicker implements IAttachPicker { constructor( @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - private readonly attachItemsProvider: IAttachProcessProvider + private readonly attachItemsProvider: IAttachProcessProvider, ) {} public showQuickPick(): Promise { @@ -23,12 +23,12 @@ export class AttachPicker implements IAttachPicker { const refreshButton = { iconPath: getIcon(REFRESH_BUTTON_ICON), - tooltip: AttachProcess.refreshList() + tooltip: AttachProcess.refreshList, }; const quickPick = this.applicationShell.createQuickPick(); - quickPick.title = AttachProcess.attachTitle(); - quickPick.placeholder = AttachProcess.selectProcessPlaceholder(); + quickPick.title = AttachProcess.attachTitle; + quickPick.placeholder = AttachProcess.selectProcessPlaceholder; quickPick.canSelectMany = false; quickPick.matchOnDescription = true; quickPick.matchOnDetail = true; @@ -39,17 +39,19 @@ export class AttachPicker implements IAttachPicker { quickPick.onDidTriggerButton( async () => { + quickPick.busy = true; const attachItems = await this.attachItemsProvider.getAttachItems(); quickPick.items = attachItems; + quickPick.busy = false; }, this, - disposables + disposables, ); quickPick.onDidAccept( () => { if (quickPick.selectedItems.length !== 1) { - reject(new Error(AttachProcess.noProcessSelected())); + reject(new Error(AttachProcess.noProcessSelected)); } const selectedId = quickPick.selectedItems[0].id; @@ -60,7 +62,7 @@ export class AttachPicker implements IAttachPicker { resolve(selectedId); }, undefined, - disposables + disposables, ); quickPick.onDidHide( @@ -68,10 +70,10 @@ export class AttachPicker implements IAttachPicker { disposables.forEach((item) => item.dispose()); quickPick.dispose(); - reject(new Error(AttachProcess.noProcessSelected())); + reject(new Error(AttachProcess.noProcessSelected)); }, undefined, - disposables + disposables, ); quickPick.show(); diff --git a/src/client/debugger/extension/attachQuickPick/provider.ts b/src/client/debugger/extension/attachQuickPick/provider.ts index fbeb3eee34ac..3626d8dfb8ce 100644 --- a/src/client/debugger/extension/attachQuickPick/provider.ts +++ b/src/client/debugger/extension/attachQuickPick/provider.ts @@ -4,9 +4,9 @@ 'use strict'; import { inject, injectable } from 'inversify'; +import { l10n } from 'vscode'; 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'; @@ -15,7 +15,7 @@ import { WmicProcessParser } from './wmicProcessParser'; export class AttachProcessProvider implements IAttachProcessProvider { constructor( @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, ) {} public getAttachItems(): Promise { @@ -23,7 +23,7 @@ export class AttachProcessProvider implements IAttachProcessProvider { processEntries.sort( ( { processName: aprocessName, commandLine: aCommandLine }, - { processName: bProcessName, commandLine: bCommandLine } + { 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) @@ -53,7 +53,7 @@ export class AttachProcessProvider implements IAttachProcessProvider { } return compare(aprocessName, bProcessName); - } + }, ); return processEntries; @@ -69,7 +69,7 @@ export class AttachProcessProvider implements IAttachProcessProvider { } else if (this.platformService.isWindows) { processCmd = WmicProcessParser.wmicCommand; } else { - throw new Error(AttachProcessLocalization.unsupportedOS().format(this.platformService.osType)); + throw new Error(l10n.t("Operating system '{0}' not supported.", this.platformService.osType)); } const processService = await this.processServiceFactory.create(); diff --git a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts index e5ab6f404a74..843369bd00c7 100644 --- a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts +++ b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts @@ -40,11 +40,11 @@ export namespace PsProcessParser { // 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=`] + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`], }; export const psDarwinCommand: ProcessListCommand = { command: 'ps', - args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`, '-c'] + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`, '-c'], }; export function parseProcesses(processes: string): IAttachItem[] { @@ -94,7 +94,7 @@ export namespace PsProcessParser { detail: cmdline, id: pid, processName: executable, - commandLine: cmdline + commandLine: cmdline, }; } } diff --git a/src/client/debugger/extension/attachQuickPick/types.ts b/src/client/debugger/extension/attachQuickPick/types.ts index c668231b29e2..5e26c1354f9e 100644 --- a/src/client/debugger/extension/attachQuickPick/types.ts +++ b/src/client/debugger/extension/attachQuickPick/types.ts @@ -3,7 +3,7 @@ 'use strict'; -import { QuickPickItem, Uri } from 'vscode'; +import { QuickPickItem } from 'vscode'; export type ProcessListCommand = { command: string; args: string[] }; @@ -26,9 +26,4 @@ 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 index d7da2b98ace3..e1faed50fc2e 100644 --- a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts +++ b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts @@ -15,7 +15,7 @@ export namespace WmicProcessParser { detail: '', id: '', processName: '', - commandLine: '' + commandLine: '', }; // Perf numbers on Win10: @@ -27,7 +27,7 @@ export namespace WmicProcessParser { // | 1308 | 1132 | export const wmicCommand: ProcessListCommand = { command: 'wmic', - args: ['process', 'get', 'Name,ProcessId,CommandLine', '/FORMAT:list'] + args: ['process', 'get', 'Name,ProcessId,CommandLine', '/FORMAT:list'], }; export function parseProcesses(processes: string): IAttachItem[] { diff --git a/src/client/debugger/extension/banner.ts b/src/client/debugger/extension/banner.ts deleted file mode 100644 index f2cea5c64fd3..000000000000 --- a/src/client/debugger/extension/banner.ts +++ /dev/null @@ -1,186 +0,0 @@ -// 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 index 655de8b6a5df..9997fb4f0509 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -4,17 +4,10 @@ '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'; +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { IDebugConfigurationService } from '../types'; +import { IDebugConfigurationResolver } from './types'; @injectable() export class PythonDebugConfigurationService implements IDebugConfigurationService { @@ -25,112 +18,46 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi @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 + token?: CancellationToken, ): Promise { if (debugConfiguration.request === 'attach') { return this.attachResolver.resolveDebugConfiguration( folder, debugConfiguration as AttachRequestArguments, - token + token, + ); + } + if (debugConfiguration.request === 'test') { + // `"request": "test"` is now deprecated. But some users might have it in their + // launch config. We get here if they triggered it using F5 or start with debugger. + throw Error( + 'This configuration can only be used by the test debugging commands. `"request": "test"` is deprecated, please keep as `"request": "launch"` and add `"purpose": ["debug-test"]` instead.', ); - } 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 undefined; } return this.launchResolver.resolveDebugConfiguration( folder, debugConfiguration as LaunchRequestArguments, - token + 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); + + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + token?: CancellationToken, + ): Promise { + function resolve(resolver: IDebugConfigurationResolver) { + return resolver.resolveDebugConfigurationWithSubstitutedVariables(folder, debugConfiguration as T, token); } + return debugConfiguration.request === 'attach' ? resolve(this.attachResolver) : resolve(this.launchResolver); } } diff --git a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts deleted file mode 100644 index 16d5c14608c8..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index 6ba5ce3a9942..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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/launchJsonReader.ts b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts new file mode 100644 index 000000000000..d5857638821a --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { parse } from 'jsonc-parser'; +import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import * as fs from '../../../../common/platform/fs-paths'; +import { getConfiguration, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { traceLog } from '../../../../logging'; + +export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { + const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); + if (!(await fs.pathExists(filename))) { + // Check launch config in the workspace file + const codeWorkspaceConfig = getConfiguration('launch', workspace); + if (!codeWorkspaceConfig.configurations || !Array.isArray(codeWorkspaceConfig.configurations)) { + return []; + } + traceLog('Using configuration in workspace'); + return codeWorkspaceConfig.configurations; + } + + const text = await fs.readFile(filename, 'utf-8'); + const parsed = parse(text, [], { allowTrailingComma: true, disallowComments: false }); + if (!parsed.configurations || !Array.isArray(parsed.configurations)) { + throw Error('Missing field in launch.json: configurations'); + } + if (!parsed.version) { + throw Error('Missing field in launch.json: version'); + } + // We do not bother ensuring each item is a DebugConfiguration... + traceLog('Using configuration in launch.json'); + return parsed.configurations; +} + +export async function getConfigurationsByUri(uri?: Uri): Promise { + if (uri) { + const workspace = getWorkspaceFolder(uri); + if (workspace) { + return getConfigurationsForWorkspace(workspace); + } + } + return []; +} diff --git a/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/src/client/debugger/extension/configuration/launch.json/updaterService.ts deleted file mode 100644 index 199cd0ecffa1..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/updaterService.ts +++ /dev/null @@ -1,171 +0,0 @@ -// 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 deleted file mode 100644 index 54e5a0a72f44..000000000000 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ /dev/null @@ -1,95 +0,0 @@ -// 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', '--noreload'], - 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 deleted file mode 100644 index 1762709e4554..000000000000 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index 45e5ee992ce3..000000000000 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ /dev/null @@ -1,75 +0,0 @@ -// 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', '--no-reload'], - 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 deleted file mode 100644 index e2d6db959d7d..000000000000 --- a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ /dev/null @@ -1,45 +0,0 @@ -// 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 deleted file mode 100644 index fe95e82b654c..000000000000 --- a/src/client/debugger/extension/configuration/providers/pidAttach.ts +++ /dev/null @@ -1,32 +0,0 @@ -// 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 deleted file mode 100644 index 0504ca398fb0..000000000000 --- a/src/client/debugger/extension/configuration/providers/providerFactory.ts +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index 9aab8b77c583..000000000000 --- a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ /dev/null @@ -1,100 +0,0 @@ -// 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 deleted file mode 100644 index 51189093ebc2..000000000000 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ /dev/null @@ -1,94 +0,0 @@ -// 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 index bb8fdf913bed..1c232f261d03 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -3,45 +3,38 @@ 'use strict'; -import { inject, injectable } from 'inversify'; +import { 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 { getOSType, OSType } from '../../../../common/utils/platform'; 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( + public async resolveDebugConfigurationWithSubstitutedVariables( folder: WorkspaceFolder | undefined, debugConfiguration: AttachRequestArguments, - _token?: CancellationToken + _token?: CancellationToken, ): Promise { - const workspaceFolder = this.getWorkspaceFolder(folder); + const workspaceFolder = AttachConfigurationResolver.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 + (item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos, ); } + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; + } return debugConfiguration; } - // tslint:disable-next-line:cyclomatic-complexity + protected async provideAttachDefaults( workspaceFolder: Uri | undefined, - debugConfiguration: AttachRequestArguments + debugConfiguration: AttachRequestArguments, ): Promise { if (!Array.isArray(debugConfiguration.debugOptions)) { debugConfiguration.debugOptions = []; @@ -50,50 +43,41 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver 0 ? pathMappings : undefined; } diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index 1132928fbd99..fde55ad8d5ea 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -3,116 +3,174 @@ '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 { getOSType, OSType } from '../../../../common/utils/platform'; +import { + getWorkspaceFolder as getVSCodeWorkspaceFolder, + getWorkspaceFolders, +} from '../../../../common/vscodeApis/workspaceApis'; +import { IInterpreterService } from '../../../../interpreter/contracts'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; import { PythonPathSource } from '../../types'; import { IDebugConfigurationResolver } from '../types'; +import { resolveVariables } from '../utils/common'; +import { getProgram } from './helper'; @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 + protected readonly configurationService: IConfigurationService, + protected readonly interpreterService: IInterpreterService, ) {} - public abstract resolveDebugConfiguration( + + // This is a legacy hook used solely for backwards-compatible manual substitution + // of ${command:python.interpreterPath} in "pythonPath", for the sake of other + // existing implementations of resolveDebugConfiguration() that may rely on it. + // + // For all future config variables, expansion should be performed by VSCode itself, + // and validation of debug configuration in derived classes should be performed in + // resolveDebugConfigurationWithSubstitutedVariables() instead, where all variables + // are already substituted. + // eslint-disable-next-line class-methods-use-this + public async resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; + } + return debugConfiguration as T; + } + + public abstract resolveDebugConfigurationWithSubstitutedVariables( folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, - token?: CancellationToken + token?: CancellationToken, ): Promise; - protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + + protected static 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 - ) { + const program = getProgram(); + const workspaceFolders = getWorkspaceFolders(); + + if (!Array.isArray(workspaceFolders) || workspaceFolders.length === 0) { return program ? Uri.file(path.dirname(program)) : undefined; } - if (this.workspaceService.workspaceFolders.length === 1) { - return this.workspaceService.workspaceFolders[0].uri; + if (workspaceFolders.length === 1) { + return workspaceFolders[0].uri; } if (program) { - const workspaceFolder = this.workspaceService.getWorkspaceFolder(Uri.file(program)); + const workspaceFolder = getVSCodeWorkspaceFolder(Uri.file(program)); if (workspaceFolder) { return workspaceFolder.uri; } } + return undefined; } - protected getProgram(): string | undefined { - const editor = this.documentManager.activeTextEditor; - if (editor && editor.document.languageId === PYTHON_LANGUAGE) { - return editor.document.fileName; - } - } - protected resolveAndUpdatePaths( + + protected async resolveAndUpdatePaths( workspaceFolder: Uri | undefined, - debugConfiguration: LaunchRequestArguments - ): void { - this.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration); - this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + debugConfiguration: LaunchRequestArguments, + ): Promise { + BaseConfigurationResolver.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration); + await this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); } - protected resolveAndUpdateEnvFilePath( + + protected static resolveAndUpdateEnvFilePath( workspaceFolder: Uri | undefined, - debugConfiguration: LaunchRequestArguments + debugConfiguration: LaunchRequestArguments, ): void { if (!debugConfiguration) { return; } if (debugConfiguration.envFile && (workspaceFolder || debugConfiguration.cwd)) { - const systemVariables = new SystemVariables( + debugConfiguration.envFile = resolveVariables( + debugConfiguration.envFile, + (workspaceFolder ? workspaceFolder.fsPath : undefined) || debugConfiguration.cwd, undefined, - (workspaceFolder ? workspaceFolder.fsPath : undefined) || debugConfiguration.cwd ); - debugConfiguration.envFile = systemVariables.resolveAny(debugConfiguration.envFile); } } - protected resolveAndUpdatePythonPath( + + protected async resolveAndUpdatePythonPath( workspaceFolder: Uri | undefined, - debugConfiguration: LaunchRequestArguments - ): void { + debugConfiguration: LaunchRequestArguments, + ): Promise { if (!debugConfiguration) { return; } if (debugConfiguration.pythonPath === '${command:python.interpreterPath}' || !debugConfiguration.pythonPath) { - const pythonPath = this.configurationService.getSettings(workspaceFolder).pythonPath; - debugConfiguration.pythonPath = pythonPath; + const interpreterPath = + (await this.interpreterService.getActiveInterpreter(workspaceFolder))?.path ?? + this.configurationService.getSettings(workspaceFolder).pythonPath; + debugConfiguration.pythonPath = interpreterPath; + } else { + debugConfiguration.pythonPath = resolveVariables( + debugConfiguration.pythonPath ? debugConfiguration.pythonPath : undefined, + workspaceFolder?.fsPath, + undefined, + ); + } + + if (debugConfiguration.python === '${command:python.interpreterPath}') { + this.pythonPathSource = PythonPathSource.settingsJson; + const interpreterPath = + (await this.interpreterService.getActiveInterpreter(workspaceFolder))?.path ?? + this.configurationService.getSettings(workspaceFolder).pythonPath; + debugConfiguration.python = interpreterPath; + } else if (debugConfiguration.python === undefined) { this.pythonPathSource = PythonPathSource.settingsJson; + debugConfiguration.python = debugConfiguration.pythonPath; } else { this.pythonPathSource = PythonPathSource.launchJson; + debugConfiguration.python = resolveVariables( + debugConfiguration.python ?? debugConfiguration.pythonPath, + workspaceFolder?.fsPath, + undefined, + ); + } + + if ( + debugConfiguration.debugAdapterPython === '${command:python.interpreterPath}' || + debugConfiguration.debugAdapterPython === undefined + ) { + debugConfiguration.debugAdapterPython = debugConfiguration.pythonPath ?? debugConfiguration.python; + } + if ( + debugConfiguration.debugLauncherPython === '${command:python.interpreterPath}' || + debugConfiguration.debugLauncherPython === undefined + ) { + debugConfiguration.debugLauncherPython = debugConfiguration.pythonPath ?? debugConfiguration.python; } + + delete debugConfiguration.pythonPath; } - protected debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + + protected static debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions): void { if (debugOptions.indexOf(debugOption) >= 0) { return; } debugOptions.push(debugOption); } - protected isLocalHost(hostName?: string) { + + protected static isLocalHost(hostName?: string): boolean { const LocalHosts = ['localhost', '127.0.0.1', '::1']; - return hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0 ? true : false; + return !!(hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0); } - protected fixUpPathMappings( + + protected static fixUpPathMappings( pathMappings: PathMapping[], defaultLocalRoot?: string, - defaultRemoteRoot?: string + defaultRemoteRoot?: string, ): PathMapping[] { if (!defaultLocalRoot) { return []; @@ -125,22 +183,24 @@ export abstract class BaseConfigurationResolver pathMappings = [ { localRoot: defaultLocalRoot, - remoteRoot: defaultRemoteRoot - } + 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 - })); + pathMappings = pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => { + const resolvedLocalRoot = resolveVariables(mappedLocalRoot, defaultLocalRoot, undefined); + return { + localRoot: resolvedLocalRoot || '', + // 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) { + if (getOSType() === OSType.Windows) { // TODO: Apply to remoteRoot too? pathMappings = pathMappings.map(({ localRoot: windowsLocalRoot, remoteRoot }) => { let localRoot = windowsLocalRoot; @@ -153,35 +213,16 @@ export abstract class BaseConfigurationResolver return pathMappings; } - protected isDebuggingFlask(debugConfiguration: Partial) { - return debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK' ? true : false; + + protected static isDebuggingFastAPI( + debugConfiguration: Partial, + ): boolean { + return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI'); } - 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); + + protected static isDebuggingFlask( + debugConfiguration: Partial, + ): boolean { + return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); } } diff --git a/src/client/debugger/extension/configuration/resolvers/helper.ts b/src/client/debugger/extension/configuration/resolvers/helper.ts index e0e31b3210c3..15be5f97538e 100644 --- a/src/client/debugger/extension/configuration/resolvers/helper.ts +++ b/src/client/debugger/extension/configuration/resolvers/helper.ts @@ -4,35 +4,54 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { ICurrentProcess, IPathUtils } from '../../../../common/types'; +import { ICurrentProcess } from '../../../../common/types'; import { EnvironmentVariables, IEnvironmentVariablesService } from '../../../../common/variables/types'; import { LaunchRequestArguments } from '../../../types'; +import { PYTHON_LANGUAGE } from '../../../../common/constants'; +import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; +import { getSearchPathEnvVarNames } from '../../../../common/utils/exec'; export const IDebugEnvironmentVariablesService = Symbol('IDebugEnvironmentVariablesService'); export interface IDebugEnvironmentVariablesService { - getEnvironmentVariables(args: LaunchRequestArguments): Promise; + getEnvironmentVariables( + args: LaunchRequestArguments, + baseVars?: EnvironmentVariables, + ): Promise; } @injectable() export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariablesService { constructor( @inject(IEnvironmentVariablesService) private envParser: IEnvironmentVariablesService, - @inject(IPathUtils) private pathUtils: IPathUtils, - @inject(ICurrentProcess) private process: ICurrentProcess + @inject(ICurrentProcess) private process: ICurrentProcess, ) {} - public async getEnvironmentVariables(args: LaunchRequestArguments): Promise { - const pathVariableName = this.pathUtils.getPathVariableName(); + + public async getEnvironmentVariables( + args: LaunchRequestArguments, + baseVars?: EnvironmentVariables, + ): Promise { + const pathVariableName = getSearchPathEnvVarNames()[0]; // 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); + args.env && Object.keys(args.env).length > 0 + ? ({ ...args.env } as Record) + : ({} as Record); const envFileVars = await this.envParser.parseFile(args.envFile, debugLaunchEnvVars); - const env = envFileVars ? { ...envFileVars! } : {}; - this.envParser.mergeVariables(debugLaunchEnvVars, env); + const env = envFileVars ? { ...envFileVars } : {}; + + // "overwrite: true" to ensure that debug-configuration env variable values + // take precedence over env file. + this.envParser.mergeVariables(debugLaunchEnvVars, env, { overwrite: true }); + if (baseVars) { + this.envParser.mergeVariables(baseVars, env, { mergeAll: true }); + } // Append the PYTHONPATH and PATH variables. - this.envParser.appendPath(env, debugLaunchEnvVars[pathVariableName]); + this.envParser.appendPath( + env, + debugLaunchEnvVars[pathVariableName] ?? debugLaunchEnvVars[pathVariableName.toUpperCase()], + ); this.envParser.appendPythonPath(env, debugLaunchEnvVars.PYTHONPATH); if (typeof env[pathVariableName] === 'string' && env[pathVariableName]!.length > 0) { @@ -75,3 +94,11 @@ export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariabl return env; } } + +export function getProgram(): string | undefined { + const activeTextEditor = getActiveTextEditor(); + if (activeTextEditor && activeTextEditor.document.languageId === PYTHON_LANGUAGE) { + return activeTextEditor.document.fileName; + } + return undefined; +} diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index 9d78188207df..3ca38fb0f710 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -7,69 +7,109 @@ 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 { getOSType, OSType } from '../../../../common/utils/platform'; +import { EnvironmentVariables } from '../../../../common/variables/types'; +import { IEnvironmentActivationService } from '../../../../interpreter/activation/types'; +import { IInterpreterService } from '../../../../interpreter/contracts'; import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; -import { IDebugEnvironmentVariablesService } from './helper'; +import { getProgram, IDebugEnvironmentVariablesService } from './helper'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../../../pythonEnvironments/creation/createEnvironmentTrigger'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { + private isCustomPythonSet = false; + 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 + @inject(IDebugEnvironmentVariablesService) private readonly debugEnvHelper: IDebugEnvironmentVariablesService, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, ) { - super(workspaceService, documentManager, platformService, configurationService); + super(configurationService, interpreterService); } + public async resolveDebugConfiguration( folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments, - _token?: CancellationToken + _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(); + this.isCustomPythonSet = debugConfiguration.python !== undefined; + if ( + debugConfiguration.name === undefined && + debugConfiguration.type === undefined && + debugConfiguration.request === undefined && + debugConfiguration.program === undefined && + debugConfiguration.env === undefined + ) { + const defaultProgram = getProgram(); + debugConfiguration.name = 'Launch'; + debugConfiguration.type = DebuggerTypeName; + debugConfiguration.request = 'launch'; + debugConfiguration.program = defaultProgram ?? ''; + debugConfiguration.env = {}; + } - config.name = 'Launch'; - config.type = DebuggerTypeName; - config.request = 'launch'; - config.program = defaultProgram ? defaultProgram : ''; - config.env = {}; + const workspaceFolder = LaunchConfigurationResolver.getWorkspaceFolder(folder); + // Pass workspace folder so we can get this when we get debug events firing. + // Do it here itself instead of `resolveDebugConfigurationWithSubstitutedVariables` which is called after + // this method, as in order to calculate substituted variables, this might be needed. + debugConfiguration.workspaceFolder = workspaceFolder?.fsPath; + await this.resolveAndUpdatePaths(workspaceFolder, debugConfiguration); + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; } + return debugConfiguration; + } - await this.provideLaunchDefaults(workspaceFolder, config); + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + _token?: CancellationToken, + ): Promise { + const workspaceFolder = LaunchConfigurationResolver.getWorkspaceFolder(folder); + await this.provideLaunchDefaults(workspaceFolder, debugConfiguration); - const isValid = await this.validateLaunchConfiguration(folder, config); + const isValid = await this.validateLaunchConfiguration(folder, debugConfiguration); if (!isValid) { - return; + return undefined; } - const dbgConfig = debugConfiguration; - if (Array.isArray(dbgConfig.debugOptions)) { - dbgConfig.debugOptions = dbgConfig.debugOptions!.filter( - (item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos + if (Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = debugConfiguration.debugOptions!.filter( + (item, pos) => debugConfiguration.debugOptions!.indexOf(item) === pos, ); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder); return debugConfiguration; } - // tslint:disable-next-line:cyclomatic-complexity + protected async provideLaunchDefaults( workspaceFolder: Uri | undefined, - debugConfiguration: LaunchRequestArguments + debugConfiguration: LaunchRequestArguments, ): Promise { - this.resolveAndUpdatePaths(workspaceFolder, debugConfiguration); + if (debugConfiguration.python === undefined) { + debugConfiguration.python = debugConfiguration.pythonPath; + } + if (debugConfiguration.debugAdapterPython === undefined) { + debugConfiguration.debugAdapterPython = debugConfiguration.pythonPath; + } + if (debugConfiguration.debugLauncherPython === undefined) { + debugConfiguration.debugLauncherPython = debugConfiguration.pythonPath; + } + delete debugConfiguration.pythonPath; + if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { debugConfiguration.cwd = workspaceFolder.fsPath; } @@ -77,10 +117,20 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver 0) { - pathMappings = this.fixUpPathMappings( + pathMappings = LaunchConfigurationResolver.fixUpPathMappings( pathMappings || [], - workspaceFolder ? workspaceFolder.fsPath : '' + 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 + debugConfiguration: LaunchRequestArguments, ): Promise { const diagnosticService = this.invalidPythonPathInDebuggerService; - return diagnosticService.validatePythonPath( - debugConfiguration.pythonPath, - this.pythonPathSource, - folder ? folder.uri : undefined - ); + for (const executable of [ + debugConfiguration.python, + debugConfiguration.debugAdapterPython, + debugConfiguration.debugLauncherPython, + ]) { + if (!(await diagnosticService.validatePythonPath(executable, this.pythonPathSource, folder?.uri))) { + return false; + } + } + return true; } } diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/debugger/extension/configuration/types.ts index 1331ea39551a..eaebf6d435c4 100644 --- a/src/client/debugger/extension/configuration/types.ts +++ b/src/client/debugger/extension/configuration/types.ts @@ -4,18 +4,18 @@ '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 + token?: CancellationToken, ): Promise; -} -export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory'); -export interface IDebugConfigurationProviderFactory { - create(configurationType: DebugConfigurationType): IDebugConfigurationProvider; + resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: T, + token?: CancellationToken, + ): Promise; } diff --git a/src/client/debugger/extension/configuration/utils/common.ts b/src/client/debugger/extension/configuration/utils/common.ts new file mode 100644 index 000000000000..3643a0c49c5d --- /dev/null +++ b/src/client/debugger/extension/configuration/utils/common.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * 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 { WorkspaceFolder } from 'vscode'; +import { getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; + +/** + * @returns whether the provided parameter is a JavaScript String or not. + */ +function isString(str: any): str is string { + if (typeof str === 'string' || str instanceof String) { + return true; + } + + return false; +} + +export function resolveVariables( + value: string | undefined, + rootFolder: string | undefined, + folder: WorkspaceFolder | undefined, +): string | undefined { + if (value) { + const workspaceFolder = folder ? getWorkspaceFolder(folder.uri) : undefined; + const variablesObject: { [key: string]: any } = {}; + variablesObject.workspaceFolder = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder; + + const regexp = /\$\{(.*?)\}/g; + return value.replace(regexp, (match: string, name: string) => { + const newValue = variablesObject[name]; + if (isString(newValue)) { + return newValue; + } + return match && (match.indexOf('env.') > 0 || match.indexOf('env:') > 0) ? '' : match; + }); + } + return value; +} diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts new file mode 100644 index 000000000000..629f8616a6d6 --- /dev/null +++ b/src/client/debugger/extension/debugCommands.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { DebugConfiguration, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager, IDebugService } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { DebugPurpose, LaunchRequestArguments } from '../types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { noop } from '../../common/utils/misc'; +import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; + +@injectable() +export class DebugCommands implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) {} + + public activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { + const interpreter = await this.interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + const config = await DebugCommands.getDebugConfiguration(file); + this.debugService.startDebugging(undefined, config); + }), + ); + return Promise.resolve(); + } + + private static async getDebugConfiguration(uri?: Uri): Promise { + const configs = (await getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); + for (const config of configs) { + if ((config as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugInTerminal)) { + if (!config.program && !config.module && !config.code) { + // This is only needed if people reuse debug-test for debug-in-terminal + config.program = uri?.fsPath ?? '${file}'; + } + // Ensure that the purpose is cleared, this is so we can track if people accidentally + // trigger this via F5 or Start with debugger. + config.purpose = []; + return config; + } + } + return { + name: `Debug ${uri ? path.basename(uri.fsPath) : 'File'}`, + type: 'python', + request: 'launch', + program: uri?.fsPath ?? '${file}', + console: 'integratedTerminal', + }; + } +} diff --git a/src/client/debugger/extension/helpers/protocolParser.ts b/src/client/debugger/extension/helpers/protocolParser.ts deleted file mode 100644 index 86a8374d51a5..000000000000 --- a/src/client/debugger/extension/helpers/protocolParser.ts +++ /dev/null @@ -1,128 +0,0 @@ -// 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 index 9a3a7bda4935..233818e00aaf 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -9,23 +9,21 @@ import { swallowExceptions } from '../../../common/utils/decorators'; import { AttachRequestArguments } from '../../types'; import { DebuggerEvents } from './constants'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; +import { DebuggerTypeName } from '../../constants'; /** * 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 + @inject(IChildProcessAttachService) private readonly childProcessAttachService: IChildProcessAttachService, ) {} @swallowExceptions('Handle child process launch') public async handleCustomEvent(event: DebugSessionCustomEvent): Promise { - if (!event) { + if (!event || event.session.configuration.type !== DebuggerTypeName) { return; } @@ -34,7 +32,7 @@ export class ChildProcessAttachEventHandler implements IDebugSessionEventHandler event.event === DebuggerEvents.PtvsdAttachToSubprocess || event.event === DebuggerEvents.DebugpyAttachToSubprocess ) { - data = event.body! as AttachRequestArguments & DebugConfiguration; + data = event.body as AttachRequestArguments & DebugConfiguration; } else { return; } diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts index c7771b845340..39556f94c87c 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -4,47 +4,47 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IDebugService, IWorkspaceService } from '../../../common/application/types'; +import { IDebugService } from '../../../common/application/types'; +import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; import { noop } from '../../../common/utils/misc'; -import { captureTelemetry } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments } from '../../types'; import { IChildProcessAttachService } from './types'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; /** * 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 - ) {} + constructor(@inject(IDebugService) private readonly debugService: IDebugService) {} - @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); + const debugSessionOption: DebugSessionOptions = { + parentSession: parentSession, + lifecycleManagedByParent: true, + }; + const launched = await this.debugService.startDebugging(folder, debugConfig, debugSessionOption); if (!launched) { - this.appShell.showErrorMessage(`Failed to launch debugger for child process ${processId}`).then(noop, noop); + showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', debugConfig.subProcessId!)).then( + noop, + noop, + ); } } private getRelatedWorkspaceFolder( - config: AttachRequestArguments & DebugConfiguration + config: AttachRequestArguments & DebugConfiguration, ): WorkspaceFolder | undefined { const workspaceFolder = config.workspaceFolder; - if (!this.workspaceService.hasWorkspaceFolders || !workspaceFolder) { + + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; + if (!hasWorkspaceFolders || !workspaceFolder) { return; } - return this.workspaceService.workspaceFolders!.find((ws) => ws.uri.fsPath === workspaceFolder); + return getWorkspaceFolders()!.find((ws) => ws.uri.fsPath === workspaceFolder); } } diff --git a/src/client/debugger/extension/hooks/constants.ts b/src/client/debugger/extension/hooks/constants.ts index b2a2e0a8ec52..3bd0b657281e 100644 --- a/src/client/debugger/extension/hooks/constants.ts +++ b/src/client/debugger/extension/hooks/constants.ts @@ -6,5 +6,5 @@ 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' + DebugpyAttachToSubprocess = 'debugpyAttach', } diff --git a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts index 3aac7df6663f..7b1dd1516abd 100644 --- a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts +++ b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts @@ -12,22 +12,22 @@ export class DebugSessionEventDispatcher { constructor( @multiInject(IDebugSessionEventHandlers) private readonly eventHandlers: IDebugSessionEventHandlers[], @inject(IDebugService) private readonly debugService: IDebugService, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + @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 + 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 + handler.handleTerminateEvent ? handler.handleTerminateEvent(e).ignoreErrors() : undefined, ); - }) + }), ); } } diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index 4e785a82f0cd..7734e87124cd 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -12,124 +12,59 @@ 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 { IDebugConfigurationResolver } from './configuration/types'; +import { DebugCommands } from './debugCommands'; import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; import { - DebugConfigurationType, IDebugAdapterDescriptorFactory, - IDebugConfigurationProvider, IDebugConfigurationService, - IDebuggerBanner, IDebugSessionLoggingFactory, - IOutdatedDebuggerPromptFactory + IOutdatedDebuggerPromptFactory, } from './types'; -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonCompletionProvider - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - InterpreterPathCommand - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonUpdaterService - ); +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IDebugConfigurationService, - PythonDebugConfigurationService + PythonDebugConfigurationService, ); - serviceManager.addSingleton(IDebuggerBanner, DebuggerBanner); serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); serviceManager.addSingleton>( IDebugConfigurationResolver, LaunchConfigurationResolver, - 'launch' + '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 + 'attach', ); serviceManager.addSingleton( IDebugEnvironmentVariablesService, - DebugEnvironmentVariablesHelper + DebugEnvironmentVariablesHelper, ); serviceManager.addSingleton( IExtensionSingleActivationService, - DebugAdapterActivator + DebugAdapterActivator, ); serviceManager.addSingleton( IDebugAdapterDescriptorFactory, - DebugAdapterDescriptorFactory + DebugAdapterDescriptorFactory, ); serviceManager.addSingleton(IDebugSessionLoggingFactory, DebugSessionLoggingFactory); serviceManager.addSingleton( IOutdatedDebuggerPromptFactory, - OutdatedDebuggerPromptFactory + OutdatedDebuggerPromptFactory, ); serviceManager.addSingleton( IAttachProcessProviderFactory, - AttachProcessProviderFactory + AttachProcessProviderFactory, ); + serviceManager.addSingleton(IExtensionSingleActivationService, DebugCommands); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index b5e6826b3b7e..4a8f35e2b808 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,53 +3,10 @@ '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'; +import { DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfigurationProvider } from 'vscode'; 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 {} @@ -62,9 +19,7 @@ export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFac 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; +export enum PythonPathSource { + launchJson = 'launch.json', + settingsJson = 'settings.json', } diff --git a/src/client/debugger/pythonDebugger.ts b/src/client/debugger/pythonDebugger.ts new file mode 100644 index 000000000000..3450e95f3cee --- /dev/null +++ b/src/client/debugger/pythonDebugger.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { extensions } from 'vscode'; + +interface IPythonDebuggerExtensionApi { + debug: { + getDebuggerPackagePath(): Promise; + }; +} + +async function activateExtension() { + const extension = extensions.getExtension('ms-python.debugpy'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + } + return extension; +} + +async function getPythonDebuggerExtensionAPI(): Promise { + const extension = await activateExtension(); + return extension?.exports as IPythonDebuggerExtensionApi; +} + +export async function getDebugpyPath(): Promise { + const api = await getPythonDebuggerExtensionAPI(); + return api?.debug.getDebuggerPackagePath() ?? ''; +} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 68ad0e49a149..1422f1aa75ab 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -5,13 +5,12 @@ import { DebugConfiguration } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { DebuggerTypeName } from './constants'; +import { DebuggerTypeName, PythonDebuggerTypeName } from './constants'; export enum DebugOptions { RedirectOutput = 'RedirectOutput', Django = 'Django', Jinja = 'Jinja', - DebugStdLib = 'DebugStdLib', Sudo = 'Sudo', Pyramid = 'Pyramid', FixFilePathCase = 'FixFilePathCase', @@ -19,18 +18,30 @@ export enum DebugOptions { UnixClient = 'UnixClient', StopOnEntry = 'StopOnEntry', ShowReturnValue = 'ShowReturnValue', - SubProcess = 'Multiprocess' + SubProcess = 'Multiprocess', +} + +export enum DebugPurpose { + DebugTest = 'debug-test', + DebugInTerminal = 'debug-in-terminal', } export type PathMapping = { localRoot: string; remoteRoot: string; }; -export type Connection = { +type Connection = { host?: string; port?: number; }; +export interface IAutomaticCodeReload { + enable?: boolean; + exclude?: string[]; + include?: string[]; + pollingInterval?: number; +} + interface ICommonDebugArguments { redirectOutput?: boolean; django?: boolean; @@ -47,8 +58,10 @@ interface ICommonDebugArguments { subProcess?: boolean; // An absolute path to local directory with source. pathMappings?: PathMapping[]; + clientOS?: 'windows' | 'unix'; } -export interface IKnownAttachDebugArguments extends ICommonDebugArguments { + +interface IKnownAttachDebugArguments extends ICommonDebugArguments { workspaceFolder?: string; customDebugger?: boolean; // localRoot and remoteRoot are deprecated (replaced by pathMappings). @@ -63,43 +76,62 @@ export interface IKnownAttachDebugArguments extends ICommonDebugArguments { listen?: Connection; } -export interface IKnownLaunchRequestArguments extends ICommonDebugArguments { +interface IKnownLaunchRequestArguments extends ICommonDebugArguments { sudo?: boolean; pyramid?: boolean; workspaceFolder?: string; // An absolute path to the program to debug. module?: string; program?: string; - pythonPath: string; + python?: string; // Automatically stop target after launch. If not specified, target does not stop. stopOnEntry?: boolean; - args: string[]; + args?: string[]; cwd?: string; debugOptions?: DebugOptions[]; env?: Record; - envFile: string; + envFile?: string; console?: ConsoleType; - // Internal field used to set custom python debug adapter (for testing) + // The following are all internal properties that are not publicly documented or + // exposed in launch.json schema for the extension. + + // Python interpreter used by the extension to spawn the debug adapter. + debugAdapterPython?: string; + + // Debug adapter to use in lieu of the one bundled with the extension. + // This must be a full path that is executable with "python "; + // for debugpy, this is ".../src/debugpy/adapter". debugAdapterPath?: string; + + // Python interpreter used by the debug adapter to spawn the debug launcher. + debugLauncherPython?: string; + + // Legacy interpreter setting. Equivalent to setting "python", "debugAdapterPython", + // and "debugLauncherPython" all at once. + pythonPath?: string; + + // Configures automatic code reloading. + autoReload?: IAutomaticCodeReload; + + // Defines where the purpose where the config should be used. + purpose?: DebugPurpose[]; } -// tslint:disable-next-line:interface-name + export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, IKnownLaunchRequestArguments, DebugConfiguration { - type: typeof DebuggerTypeName; + type: typeof DebuggerTypeName | typeof PythonDebuggerTypeName; } -// tslint:disable-next-line:interface-name export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments, IKnownAttachDebugArguments, DebugConfiguration { - type: typeof DebuggerTypeName; + type: typeof DebuggerTypeName | typeof PythonDebuggerTypeName; } -// tslint:disable-next-line:interface-name export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments {} export type ConsoleType = 'internalConsole' | 'integratedTerminal' | 'externalTerminal'; diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts new file mode 100644 index 000000000000..d0003c895517 --- /dev/null +++ b/src/client/deprecatedProposedApi.ts @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, EventEmitter } from 'vscode'; +import { arePathsSame } from './common/platform/fs-paths'; +import { IExtensions, IInterpreterPathService, Resource } from './common/types'; +import { + EnvironmentsChangedParams, + ActiveEnvironmentChangedParams, + EnvironmentDetailsOptions, + EnvironmentDetails, + DeprecatedProposedAPI, +} from './deprecatedProposedApiTypes'; +import { IInterpreterService } from './interpreter/contracts'; +import { IServiceContainer } from './ioc/types'; +import { traceVerbose, traceWarn } from './logging'; +import { PythonEnvInfo } from './pythonEnvironments/base/info'; +import { getEnvPath } from './pythonEnvironments/base/info/env'; +import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; + +const onDidInterpretersChangedEvent = new EventEmitter(); +/** + * @deprecated Will be removed soon. + */ +export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void { + onDidInterpretersChangedEvent.fire(e); +} + +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +/** + * @deprecated Will be removed soon. + */ +export function reportActiveInterpreterChangedDeprecated(e: ActiveEnvironmentChangedParams): void { + onDidActiveInterpreterChangedEvent.fire(e); +} + +function getVersionString(env: PythonEnvInfo): string[] { + const ver = [`${env.version.major}`, `${env.version.minor}`, `${env.version.micro}`]; + if (env.version.release) { + ver.push(`${env.version.release}`); + if (env.version.sysVersion) { + ver.push(`${env.version.release}`); + } + } + return ver; +} + +/** + * Returns whether the path provided matches the environment. + * @param path Path to environment folder or path to interpreter that uniquely identifies an environment. + * @param env Environment to match with. + */ +function isEnvSame(path: string, env: PythonEnvInfo) { + return arePathsSame(path, env.location) || arePathsSame(path, env.executable.filename); +} + +export function buildDeprecatedProposedApi( + discoveryApi: IDiscoveryAPI, + serviceContainer: IServiceContainer, +): DeprecatedProposedAPI { + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterService = serviceContainer.get(IInterpreterService); + const extensions = serviceContainer.get(IExtensions); + const warningLogged = new Set(); + function sendApiTelemetry(apiName: string, warnLog = true) { + extensions + .determineExtensionFromCallStack() + .then((info) => { + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + }); + traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); + if (warnLog && !warningLogged.has(info.extensionId)) { + traceWarn( + `${info.extensionId} extension is using deprecated python APIs which will be removed soon.`, + ); + warningLogged.add(info.extensionId); + } + }) + .ignoreErrors(); + } + + const proposed: DeprecatedProposedAPI = { + environment: { + async getExecutionDetails(resource?: Resource) { + sendApiTelemetry('deprecated.getExecutionDetails'); + const env = await interpreterService.getActiveInterpreter(resource); + return env ? { execCommand: [env.path] } : { execCommand: undefined }; + }, + async getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('deprecated.getActiveEnvironmentPath'); + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return getEnvPath(env.path, env.envPath); + }, + async getEnvironmentDetails( + path: string, + options?: EnvironmentDetailsOptions, + ): Promise { + sendApiTelemetry('deprecated.getEnvironmentDetails'); + let env: PythonEnvInfo | undefined; + if (options?.useCache) { + env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); + } + if (!env) { + env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + } + return { + interpreterPath: env.executable.filename, + envFolderPath: env.location.length ? env.location : undefined, + version: getVersionString(env), + environmentType: [env.kind], + metadata: { + sysPrefix: env.executable.sysPrefix, + bitness: env.arch, + project: env.searchLocation, + }, + }; + }, + getEnvironmentPaths() { + sendApiTelemetry('deprecated.getEnvironmentPaths'); + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + setActiveEnvironment(path: string, resource?: Resource): Promise { + sendApiTelemetry('deprecated.setActiveEnvironment'); + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + async refreshEnvironment() { + sendApiTelemetry('deprecated.refreshEnvironment'); + await discoveryApi.triggerRefresh(); + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + sendApiTelemetry('deprecated.getRefreshPromise'); + return discoveryApi.getRefreshPromise(options); + }, + get onDidChangeExecutionDetails() { + sendApiTelemetry('deprecated.onDidChangeExecutionDetails', false); + return interpreterService.onDidChangeInterpreterConfiguration; + }, + get onDidEnvironmentsChanged() { + sendApiTelemetry('deprecated.onDidEnvironmentsChanged', false); + return onDidInterpretersChangedEvent.event; + }, + get onDidActiveEnvironmentChanged() { + sendApiTelemetry('deprecated.onDidActiveEnvironmentChanged', false); + return onDidActiveInterpreterChangedEvent.event; + }, + get onRefreshProgress() { + sendApiTelemetry('deprecated.onRefreshProgress', false); + return discoveryApi.onProgress; + }, + }, + }; + return proposed; +} diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts new file mode 100644 index 000000000000..eb76d61dc907 --- /dev/null +++ b/src/client/deprecatedProposedApiTypes.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, Event } from 'vscode'; +import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; +import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; +import { Resource } from './api/types'; + +export interface EnvironmentDetailsOptions { + useCache: boolean; +} + +export interface EnvironmentDetails { + interpreterPath: string; + envFolderPath?: string; + version: string[]; + environmentType: PythonEnvKind[]; + metadata: Record; +} + +export interface EnvironmentsChangedParams { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path?: string; + type: 'add' | 'remove' | 'update' | 'clear-all'; +} + +export interface ActiveEnvironmentChangedParams { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path: string; + resource?: Uri; +} + +/** + * @deprecated Use {@link ProposedExtensionAPI} instead. + */ +export interface DeprecatedProposedAPI { + /** + * @deprecated Use {@link ProposedExtensionAPI.environments} instead. This will soon be removed. + */ + environment: { + /** + * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. + */ + readonly onDidChangeExecutionDetails: Event; + /** + * Returns all the details the consumer needs to execute code within the selected environment, + * corresponding to the specified resource taking into account any workspace-specific settings + * for the workspace to which this resource belongs. + * @param {Resource} [resource] A resource for which the setting is asked for. + * * When no resource is provided, the setting scoped to the first workspace folder is returned. + * * If no folder is present, it returns the global setting. + */ + getExecutionDetails( + resource?: Resource, + ): Promise<{ + /** + * E.g of execution commands returned could be, + * * `['']` + * * `['']` + * * `['conda', 'run', 'python']` which is used to run from within Conda environments. + * or something similar for some other Python environments. + * + * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. + * Otherwise, join the items returned using space to construct the full execution command. + */ + execCommand: string[] | undefined; + }>; + /** + * Returns the path to the python binary selected by the user or as in the settings. + * This is just the path to the python binary, this does not provide activation or any + * other activation command. The `resource` if provided will be used to determine the + * python binary in a multi-root scenario. If resource is `undefined` then the API + * returns what ever is set for the workspace. + * @param resource : Uri of a file or workspace + */ + getActiveEnvironmentPath(resource?: Resource): Promise; + /** + * Returns details for the given interpreter. Details such as absolute interpreter path, + * version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under + * metadata field. + * @param path : Full path to environment folder or interpreter whose details you need. + * @param options : [optional] + * * useCache : When true, cache is checked first for any data, returns even if there + * is partial data. + */ + getEnvironmentDetails( + path: string, + options?: EnvironmentDetailsOptions, + ): Promise; + /** + * Returns paths to environments that uniquely identifies an environment found by the extension + * at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it + * will *not* wait for the refresh to finish. This will return what is known so far. To get + * complete list `await` on promise returned by `getRefreshPromise()`. + * + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + getEnvironmentPaths(): Promise; + /** + * Sets the active environment path for the python extension for the resource. Configuration target + * will always be the workspace folder. + * @param path : Full path to environment folder or interpreter to set. + * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace + * folder. + */ + setActiveEnvironment(path: string, resource?: Resource): Promise; + /** + * This API will re-trigger environment discovery. Extensions can wait on the returned + * promise to get the updated environment list. If there is a refresh already going on + * then it returns the promise for that refresh. + * @param options : [optional] + * * clearCache : When true, this will clear the cache before environment refresh + * is triggered. + */ + refreshEnvironment(): Promise; + /** + * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant + * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of + * the entire collection. + */ + readonly onRefreshProgress: Event; + /** + * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active + * refreshes going on. + */ + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + onDidEnvironmentsChanged: Event; + /** + * @deprecated Use {@link ProposedExtensionAPI.environments} `onDidChangeActiveEnvironmentPath` instead. This will soon be removed. + */ + onDidActiveEnvironmentChanged: Event; + }; +} diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts new file mode 100644 index 000000000000..5edfb712072e --- /dev/null +++ b/src/client/envExt/api.internal.ts @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EventEmitter, Terminal, Uri, Disposable } from 'vscode'; +import { getExtension } from '../common/vscodeApis/extensionsApi'; +import { + GetEnvironmentScope, + PythonBackgroundRunOptions, + PythonEnvironment, + PythonEnvironmentApi, + PythonProcess, + RefreshEnvironmentsScope, + DidChangeEnvironmentEventArgs, +} from './types'; +import { executeCommand } from '../common/vscodeApis/commandApis'; +import { getConfiguration, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; +import { traceError, traceLog } from '../logging'; +import { Interpreters } from '../common/utils/localize'; + +export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; + +export function isEnvExtensionInstalled(): boolean { + return !!getExtension(ENVS_EXTENSION_ID); +} + +/** + * Returns true if the Python Environments extension is installed and not explicitly + * disabled by the user. Mirrors the envs extension's own activation logic: it + * deactivates only when `python.useEnvironmentsExtension` is explicitly set to false + * at the global, workspace, or workspace-folder level. + */ +export function shouldEnvExtHandleActivation(): boolean { + if (!isEnvExtensionInstalled()) { + return false; + } + const config = getConfiguration('python'); + const inspection = config.inspect('useEnvironmentsExtension'); + if (inspection?.globalValue === false || inspection?.workspaceValue === false) { + return false; + } + // The envs extension also checks folder-scoped settings in multi-root workspaces. + // Any single folder with the setting set to false causes the envs extension to + // deactivate entirely (window-wide), so we must mirror that here. + const workspaceFolders = getWorkspaceFolders(); + if (workspaceFolders) { + for (const folder of workspaceFolders) { + const folderConfig = getConfiguration('python', folder.uri); + const folderInspection = folderConfig.inspect('useEnvironmentsExtension'); + if (folderInspection?.workspaceFolderValue === false) { + return false; + } + } + } + return true; +} + +let _useExt: boolean | undefined; +export function useEnvExtension(): boolean { + if (_useExt !== undefined) { + return _useExt; + } + const config = getConfiguration('python'); + const inExpSetting = config?.get('useEnvironmentsExtension', false) ?? false; + // If extension is installed and in experiment, then use it. + _useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting; + return _useExt; +} + +const onDidChangeEnvironmentEnvExtEmitter: EventEmitter = new EventEmitter< + DidChangeEnvironmentEventArgs +>(); +export function onDidChangeEnvironmentEnvExt( + listener: (e: DidChangeEnvironmentEventArgs) => unknown, + thisArgs?: unknown, + disposables?: Disposable[], +): Disposable { + return onDidChangeEnvironmentEnvExtEmitter.event(listener, thisArgs, disposables); +} + +let _extApi: PythonEnvironmentApi | undefined; +export async function getEnvExtApi(): Promise { + if (_extApi) { + return _extApi; + } + const extension = getExtension(ENVS_EXTENSION_ID); + if (!extension) { + traceError(Interpreters.envExtActivationFailed); + throw new Error('Python Environments extension not found.'); + } + if (!extension?.isActive) { + try { + await extension.activate(); + } catch (ex) { + traceError(Interpreters.envExtActivationFailed, ex); + throw ex; + } + } + + traceLog(Interpreters.envExtDiscoveryAttribution); + + _extApi = extension.exports as PythonEnvironmentApi; + _extApi.onDidChangeEnvironment((e) => { + onDidChangeEnvironmentEnvExtEmitter.fire(e); + }); + + return _extApi; +} + +export async function runInBackground( + environment: PythonEnvironment, + options: PythonBackgroundRunOptions, +): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.runInBackground(environment, options); +} + +export async function getEnvironment(scope: GetEnvironmentScope): Promise { + const envExtApi = await getEnvExtApi(); + const env = await envExtApi.getEnvironment(scope); + if (!env) { + traceLog(Interpreters.envExtNoActiveEnvironment); + } + return env; +} + +export async function resolveEnvironment(pythonPath: string): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.resolveEnvironment(Uri.file(pythonPath)); +} + +export async function refreshEnvironments(scope: RefreshEnvironmentsScope): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.refreshEnvironments(scope); +} + +export async function runInTerminal( + resource: Uri | undefined, + args?: string[], + cwd?: string | Uri, + show?: boolean, +): Promise { + const envExtApi = await getEnvExtApi(); + const env = await getEnvironment(resource); + const project = resource ? envExtApi.getPythonProject(resource) : undefined; + if (env && resource) { + return envExtApi.runInTerminal(env, { + cwd: cwd ?? project?.uri ?? process.cwd(), + args, + show, + }); + } + throw new Error('Invalid arguments to run in terminal'); +} + +export async function runInDedicatedTerminal( + resource: Uri | undefined, + args?: string[], + cwd?: string | Uri, + show?: boolean, +): Promise { + const envExtApi = await getEnvExtApi(); + const env = await getEnvironment(resource); + const project = resource ? envExtApi.getPythonProject(resource) : undefined; + if (env) { + return envExtApi.runInDedicatedTerminal(resource ?? 'global', env, { + cwd: cwd ?? project?.uri ?? process.cwd(), + args, + show, + }); + } + throw new Error('Invalid arguments to run in dedicated terminal'); +} + +export async function clearCache(): Promise { + const envExtApi = await getEnvExtApi(); + if (envExtApi) { + await executeCommand('python-envs.clearCache'); + } +} diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts new file mode 100644 index 000000000000..6f2e60774033 --- /dev/null +++ b/src/client/envExt/api.legacy.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Terminal, Uri } from 'vscode'; +import { getEnvExtApi, getEnvironment } from './api.internal'; +import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info'; +import { PythonEnvironment, PythonTerminalCreateOptions } from './types'; +import { Architecture } from '../common/utils/platform'; +import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; +import { PythonEnvType } from '../pythonEnvironments/base/info'; +import { traceError } from '../logging'; +import { reportActiveInterpreterChanged } from '../environmentApi'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +function toEnvironmentType(pythonEnv: PythonEnvironment): EnvironmentType { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { + return EnvironmentType.System; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) { + return EnvironmentType.Venv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) { + return EnvironmentType.VirtualEnv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return EnvironmentType.Conda; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) { + return EnvironmentType.Pipenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) { + return EnvironmentType.Poetry; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) { + return EnvironmentType.Pyenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) { + return EnvironmentType.Hatch; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) { + return EnvironmentType.Pixi; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) { + return EnvironmentType.VirtualEnvWrapper; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) { + return EnvironmentType.ActiveState; + } + return EnvironmentType.Unknown; +} + +function getEnvType(kind: EnvironmentType): PythonEnvType | undefined { + switch (kind) { + case EnvironmentType.Pipenv: + case EnvironmentType.VirtualEnv: + case EnvironmentType.Pyenv: + case EnvironmentType.Venv: + case EnvironmentType.Poetry: + case EnvironmentType.Hatch: + case EnvironmentType.Pixi: + case EnvironmentType.VirtualEnvWrapper: + case EnvironmentType.ActiveState: + return PythonEnvType.Virtual; + + case EnvironmentType.Conda: + return PythonEnvType.Conda; + + case EnvironmentType.MicrosoftStore: + case EnvironmentType.Global: + case EnvironmentType.System: + default: + return undefined; + } +} + +function toLegacyType(env: PythonEnvironment): PythonEnvironmentLegacy { + const ver = parseVersion(env.version); + const envType = toEnvironmentType(env); + return { + id: env.execInfo.run.executable, + displayName: env.displayName, + detailedDisplayName: env.name, + envType, + envPath: env.sysPrefix, + type: getEnvType(envType), + path: env.execInfo.run.executable, + version: { + raw: env.version, + major: ver.major, + minor: ver.minor, + patch: ver.micro, + build: [], + prerelease: [], + }, + sysVersion: env.version, + architecture: Architecture.x64, + sysPrefix: env.sysPrefix, + }; +} + +const previousEnvMap = new Map(); +export async function getActiveInterpreterLegacy(resource?: Uri): Promise { + const api = await getEnvExtApi(); + const uri = resource ? api.getPythonProject(resource)?.uri : undefined; + + const pythonEnv = await getEnvironment(resource); + const oldEnv = previousEnvMap.get(uri?.fsPath || ''); + const newEnv = pythonEnv ? toLegacyType(pythonEnv) : undefined; + + const folders = getWorkspaceFolders() ?? []; + const shouldReport = + (folders.length === 0 && resource === undefined) || (folders.length > 0 && resource !== undefined); + if (shouldReport && newEnv && oldEnv?.envId.id !== pythonEnv?.envId.id) { + reportActiveInterpreterChanged({ + resource: getWorkspaceFolder(resource), + path: newEnv.path, + }); + previousEnvMap.set(uri?.fsPath || '', pythonEnv); + } + return pythonEnv ? toLegacyType(pythonEnv) : undefined; +} + +export async function setInterpreterLegacy(pythonPath: string, uri: Uri | undefined): Promise { + const api = await getEnvExtApi(); + const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath)); + if (!pythonEnv) { + traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`); + return; + } + await api.setEnvironment(uri, pythonEnv); +} + +export async function resetInterpreterLegacy(uri: Uri | undefined): Promise { + const api = await getEnvExtApi(); + await api.setEnvironment(uri, undefined); +} + +export async function ensureTerminalLegacy( + resource: Uri | undefined, + options?: PythonTerminalCreateOptions, +): Promise { + const api = await getEnvExtApi(); + const pythonEnv = await api.getEnvironment(resource); + const project = resource ? api.getPythonProject(resource) : undefined; + + if (pythonEnv && project) { + const fixedOptions = options ? { ...options } : { cwd: project.uri }; + const terminal = await api.createTerminal(pythonEnv, fixedOptions); + return terminal; + } + traceError('ensureTerminalLegacy - Did not return terminal successfully.'); + traceError( + 'ensureTerminalLegacy - pythonEnv:', + pythonEnv + ? `id=${pythonEnv.envId.id}, managerId=${pythonEnv.envId.managerId}, name=${pythonEnv.name}, version=${pythonEnv.version}, executable=${pythonEnv.execInfo.run.executable}` + : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - project:', + project ? `name=${project.name}, uri=${project.uri.toString()}` : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - options:', + options + ? `name=${options.name}, cwd=${options.cwd?.toString()}, hideFromUser=${options.hideFromUser}` + : 'undefined', + ); + traceError('ensureTerminalLegacy - resource:', resource?.toString() || 'undefined'); + + throw new Error('Invalid arguments to create terminal'); +} diff --git a/src/client/envExt/envExtApi.ts b/src/client/envExt/envExtApi.ts new file mode 100644 index 000000000000..34f42f0d6954 --- /dev/null +++ b/src/client/envExt/envExtApi.ts @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ + +import * as path from 'path'; +import { Event, EventEmitter, Disposable, Uri } from 'vscode'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType, PythonVersion } from '../pythonEnvironments/base/info'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../pythonEnvironments/base/locator'; +import { PythonEnvCollectionChangedEvent } from '../pythonEnvironments/base/watcher'; +import { getEnvExtApi } from './api.internal'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { StopWatch } from '../common/utils/stopWatch'; +import { traceError, traceLog, traceWarn } from '../logging'; +import { + DidChangeEnvironmentsEventArgs, + EnvironmentChangeKind, + PythonEnvironment, + PythonEnvironmentApi, +} from './types'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { Architecture, isWindows } from '../common/utils/platform'; +import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; +import { Interpreters } from '../common/utils/localize'; + +function getKind(pythonEnv: PythonEnvironment): PythonEnvKind { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { + return PythonEnvKind.System; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return PythonEnvKind.Conda; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) { + return PythonEnvKind.Venv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) { + return PythonEnvKind.VirtualEnv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) { + return PythonEnvKind.VirtualEnvWrapper; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) { + return PythonEnvKind.Pyenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) { + return PythonEnvKind.Pipenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) { + return PythonEnvKind.Poetry; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) { + return PythonEnvKind.Pixi; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) { + return PythonEnvKind.Hatch; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) { + return PythonEnvKind.ActiveState; + } + + return PythonEnvKind.Unknown; +} + +function makeExecutablePath(prefix?: string): string { + if (!prefix) { + return process.platform === 'win32' ? 'python.exe' : 'python'; + } + return process.platform === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'python'); +} + +function getExecutable(pythonEnv: PythonEnvironment): string { + if (pythonEnv.execInfo?.run?.executable) { + return pythonEnv.execInfo?.run?.executable; + } + + const basename = path.basename(pythonEnv.environmentPath.fsPath).toLowerCase(); + if (isWindows() && basename.startsWith('python') && basename.endsWith('.exe')) { + return pythonEnv.environmentPath.fsPath; + } + + if (!isWindows() && basename.startsWith('python')) { + return pythonEnv.environmentPath.fsPath; + } + + return makeExecutablePath(pythonEnv.sysPrefix); +} + +function getLocation(pythonEnv: PythonEnvironment): string { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return pythonEnv.sysPrefix; + } + + return pythonEnv.environmentPath.fsPath; +} + +function getEnvType(kind: PythonEnvKind): PythonEnvType | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + case PythonEnvKind.Pyenv: + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + case PythonEnvKind.Pipenv: + case PythonEnvKind.ActiveState: + case PythonEnvKind.Hatch: + case PythonEnvKind.Pixi: + return PythonEnvType.Virtual; + + case PythonEnvKind.Conda: + return PythonEnvType.Conda; + + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + case PythonEnvKind.MicrosoftStore: + default: + return undefined; + } +} + +function toPythonEnvInfo(pythonEnv: PythonEnvironment): PythonEnvInfo | undefined { + const kind = getKind(pythonEnv); + const arch = Architecture.x64; + const version: PythonVersion = parseVersion(pythonEnv.version); + const { name, displayName, sysPrefix } = pythonEnv; + const executable = getExecutable(pythonEnv); + const location = getLocation(pythonEnv); + + return { + name, + location, + kind, + id: executable, + executable: { + filename: executable, + sysPrefix, + ctime: -1, + mtime: -1, + }, + version: { + sysVersion: pythonEnv.version, + major: version.major, + minor: version.minor, + micro: version.micro, + }, + arch, + distro: { + org: '', + }, + source: [], + detailedDisplayName: displayName, + display: displayName, + type: getEnvType(kind), + }; +} + +function hasChanged(old: PythonEnvInfo, newEnv: PythonEnvInfo): boolean { + if (old.executable.filename !== newEnv.executable.filename) { + return true; + } + if (old.version.major !== newEnv.version.major) { + return true; + } + if (old.version.minor !== newEnv.version.minor) { + return true; + } + if (old.version.micro !== newEnv.version.micro) { + return true; + } + if (old.location !== newEnv.location) { + return true; + } + if (old.kind !== newEnv.kind) { + return true; + } + if (old.arch !== newEnv.arch) { + return true; + } + + return false; +} + +class EnvExtApis implements IDiscoveryAPI, Disposable { + private _onProgress: EventEmitter; + + private _onChanged: EventEmitter; + + private _refreshPromise?: Deferred; + + private _envs: PythonEnvInfo[] = []; + + refreshState: ProgressReportStage; + + private _disposables: Disposable[] = []; + + constructor(private envExtApi: PythonEnvironmentApi) { + this._onProgress = new EventEmitter(); + this._onChanged = new EventEmitter(); + + this.onProgress = this._onProgress.event; + this.onChanged = this._onChanged.event; + + this.refreshState = ProgressReportStage.idle; + this._disposables.push( + this._onProgress, + this._onChanged, + this.envExtApi.onDidChangeEnvironments((e) => this.onDidChangeEnvironments(e)), + this.envExtApi.onDidChangeEnvironment((e) => { + this._onChanged.fire({ + type: FileChangeType.Changed, + searchLocation: e.uri, + old: e.old ? toPythonEnvInfo(e.old) : undefined, + new: e.new ? toPythonEnvInfo(e.new) : undefined, + }); + }), + ); + } + + onProgress: Event; + + onChanged: Event; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise { + const stopwatch = new StopWatch(); + traceLog('Native locator: Refresh started'); + if (this.refreshState === ProgressReportStage.discoveryStarted && this._refreshPromise?.promise) { + return this._refreshPromise?.promise; + } + + this.refreshState = ProgressReportStage.discoveryStarted; + this._onProgress.fire({ stage: this.refreshState }); + this._refreshPromise = createDeferred(); + + const SLOW_DISCOVERY_THRESHOLD_MS = 25_000; + const slowDiscoveryTimer = setTimeout(() => { + traceWarn(Interpreters.envExtDiscoverySlow); + }, SLOW_DISCOVERY_THRESHOLD_MS); + + setImmediate(async () => { + try { + await this.envExtApi.refreshEnvironments(undefined); + if (this._envs.length === 0) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } + this._refreshPromise?.resolve(); + } catch (error) { + traceError(Interpreters.envExtDiscoveryFailed, error); + this._refreshPromise?.reject(error); + } finally { + clearTimeout(slowDiscoveryTimer); + traceLog(`Native locator: Refresh finished in ${stopwatch.elapsedTime} ms`); + this.refreshState = ProgressReportStage.discoveryFinished; + this._refreshPromise = undefined; + this._onProgress.fire({ stage: this.refreshState }); + } + }); + + return this._refreshPromise?.promise; + } + + getEnvs(_query?: PythonLocatorQuery): PythonEnvInfo[] { + return this._envs; + } + + private addEnv(pythonEnv: PythonEnvironment, searchLocation?: Uri): PythonEnvInfo | undefined { + const info = toPythonEnvInfo(pythonEnv); + if (info) { + const old = this._envs.find((item) => item.executable.filename === info.executable.filename); + if (old) { + this._envs = this._envs.filter((item) => item.executable.filename !== info.executable.filename); + this._envs.push(info); + if (hasChanged(old, info)) { + this._onChanged.fire({ type: FileChangeType.Changed, old, new: info, searchLocation }); + } + } else { + this._envs.push(info); + this._onChanged.fire({ type: FileChangeType.Created, new: info, searchLocation }); + } + } + + return info; + } + + private removeEnv(env: PythonEnvInfo | string): void { + if (typeof env === 'string') { + const old = this._envs.find((item) => item.executable.filename === env); + this._envs = this._envs.filter((item) => item.executable.filename !== env); + this._onChanged.fire({ type: FileChangeType.Deleted, old }); + return; + } + this._envs = this._envs.filter((item) => item.executable.filename !== env.executable.filename); + this._onChanged.fire({ type: FileChangeType.Deleted, old: env }); + } + + async resolveEnv(envPath?: string): Promise { + if (envPath === undefined) { + return undefined; + } + try { + const pythonEnv = await this.envExtApi.resolveEnvironment(Uri.file(envPath)); + if (pythonEnv) { + return this.addEnv(pythonEnv); + } + } catch (error) { + traceError( + `Failed to resolve environment "${envPath}" via the Python Environments extension (ms-python.vscode-python-envs). Check the "Python Environments" output channel for details.`, + error, + ); + } + return undefined; + } + + dispose(): void { + this._disposables.forEach((d) => d.dispose()); + } + + onDidChangeEnvironments(e: DidChangeEnvironmentsEventArgs): void { + e.forEach((item) => { + if (item.kind === EnvironmentChangeKind.remove) { + this.removeEnv(item.environment.environmentPath.fsPath); + } + if (item.kind === EnvironmentChangeKind.add) { + this.addEnv(item.environment); + } + }); + } +} + +export async function createEnvExtApi(disposables: Disposable[]): Promise { + const api = new EnvExtApis(await getEnvExtApi()); + disposables.push(api); + return api; +} diff --git a/src/client/envExt/types.ts b/src/client/envExt/types.ts new file mode 100644 index 000000000000..707d641bbfe8 --- /dev/null +++ b/src/client/envExt/types.ts @@ -0,0 +1,1274 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Disposable, + Event, + FileChangeType, + LogOutputChannel, + MarkdownString, + TaskExecution, + Terminal, + TerminalOptions, + ThemeIcon, + Uri, +} from 'vscode'; + +/** + * The path to an icon, or a theme-specific configuration of icons. + */ +export type IconPath = + | Uri + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } + | ThemeIcon; + +/** + * Options for executing a Python executable. + */ +export interface PythonCommandRunConfiguration { + /** + * Path to the binary like `python.exe` or `python3` to execute. This should be an absolute path + * to an executable that can be spawned. + */ + executable: string; + + /** + * Arguments to pass to the python executable. These arguments will be passed on all execute calls. + * This is intended for cases where you might want to do interpreter specific flags. + */ + args?: string[]; +} + +/** + * Contains details on how to use a particular python environment + * + * Running In Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.activatedRun} is provided, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + * Creating a Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + */ +export interface PythonEnvironmentExecutionInfo { + /** + * Details on how to run the python executable. + */ + run: PythonCommandRunConfiguration; + + /** + * Details on how to run the python executable after activating the environment. + * If set this will overrides the {@link PythonEnvironmentExecutionInfo.run} command. + */ + activatedRun?: PythonCommandRunConfiguration; + + /** + * Details on how to activate an environment. + */ + activation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to activate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.activation} if set. + */ + shellActivation?: Map; + + /** + * Details on how to deactivate an environment. + */ + deactivation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to deactivate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.deactivation} if set. + */ + shellDeactivation?: Map; +} + +/** + * Interface representing the ID of a Python environment. + */ +export interface PythonEnvironmentId { + /** + * The unique identifier of the Python environment. + */ + id: string; + + /** + * The ID of the manager responsible for the Python environment. + */ + managerId: string; +} + +/** + * Display information for an environment group. + */ +export interface EnvironmentGroupInfo { + /** + * The name of the environment group. This is used as an identifier for the group. + * + * Note: The first instance of the group with the given name will be used in the UI. + */ + readonly name: string; + + /** + * The description of the environment group. + */ + readonly description?: string; + + /** + * The tooltip for the environment group, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Interface representing information about a Python environment. + */ +export interface PythonEnvironmentInfo { + /** + * The name of the Python environment. + */ + readonly name: string; + + /** + * The display name of the Python environment. + */ + readonly displayName: string; + + /** + * The short display name of the Python environment. + */ + readonly shortDisplayName?: string; + + /** + * The display path of the Python environment. + */ + readonly displayPath: string; + + /** + * The version of the Python environment. + */ + readonly version: string; + + /** + * Path to the python binary or environment folder. + */ + readonly environmentPath: Uri; + + /** + * The description of the Python environment. + */ + readonly description?: string; + + /** + * The tooltip for the Python environment, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python environment, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Information on how to execute the Python environment. This is required for executing Python code in the environment. + */ + readonly execInfo: PythonEnvironmentExecutionInfo; + + /** + * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. + * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. + */ + readonly sysPrefix: string; + + /** + * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. + */ + readonly group?: string | EnvironmentGroupInfo; +} + +/** + * Interface representing a Python environment. + */ +export interface PythonEnvironment extends PythonEnvironmentInfo { + /** + * The ID of the Python environment. + */ + readonly envId: PythonEnvironmentId; +} + +/** + * Type representing the scope for setting a Python environment. + * Can be undefined or a URI. + */ +export type SetEnvironmentScope = undefined | Uri | Uri[]; + +/** + * Type representing the scope for getting a Python environment. + * Can be undefined or a URI. + */ +export type GetEnvironmentScope = undefined | Uri; + +/** + * Type representing the scope for creating a Python environment. + * Can be a Python project or 'global'. + */ +export type CreateEnvironmentScope = Uri | Uri[] | 'global'; +/** + * The scope for which environments are to be refreshed. + * - `undefined`: Search for environments globally and workspaces. + * - {@link Uri}: Environments in the workspace/folder or associated with the Uri. + */ +export type RefreshEnvironmentsScope = Uri | undefined; + +/** + * The scope for which environments are required. + * - `"all"`: All environments. + * - `"global"`: Python installations that are usually a base for creating virtual environments. + * - {@link Uri}: Environments for the workspace/folder/file pointed to by the Uri. + */ +export type GetEnvironmentsScope = Uri | 'all' | 'global'; + +/** + * Event arguments for when the current Python environment changes. + */ +export type DidChangeEnvironmentEventArgs = { + /** + * The URI of the environment that changed. + */ + readonly uri: Uri | undefined; + + /** + * The old Python environment before the change. + */ + readonly old: PythonEnvironment | undefined; + + /** + * The new Python environment after the change. + */ + readonly new: PythonEnvironment | undefined; +}; + +/** + * Enum representing the kinds of environment changes. + */ +export enum EnvironmentChangeKind { + /** + * Indicates that an environment was added. + */ + add = 'add', + + /** + * Indicates that an environment was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when the list of Python environments changes. + */ +export type DidChangeEnvironmentsEventArgs = { + /** + * The kind of change that occurred (add or remove). + */ + kind: EnvironmentChangeKind; + + /** + * The Python environment that was added or removed. + */ + environment: PythonEnvironment; +}[]; + +/** + * Type representing the context for resolving a Python environment. + */ +export type ResolveEnvironmentContext = Uri; + +export interface QuickCreateConfig { + /** + * The description of the quick create step. + */ + readonly description: string; + + /** + * The detail of the quick create step. + */ + readonly detail?: string; +} + +/** + * Interface representing an environment manager. + */ +export interface EnvironmentManager { + /** + * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + readonly name: string; + + /** + * The display name of the environment manager. + */ + readonly displayName?: string; + + /** + * The preferred package manager ID for the environment manager. This is a combination + * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. + * `.:` + * + * @example + * 'ms-python.python:pip' + */ + readonly preferredPackageManagerId: string; + + /** + * The description of the environment manager. + */ + readonly description?: string; + + /** + * The tooltip for the environment manager, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the environment manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The log output channel for the environment manager. + */ + readonly log?: LogOutputChannel; + + /** + * The quick create details for the environment manager. Having this method also enables the quick create feature + * for the environment manager. Should Implement {@link EnvironmentManager.create} to support quick create. + */ + quickCreateConfig?(): QuickCreateConfig | undefined; + + /** + * Creates a new Python environment within the specified scope. + * @param scope - The scope within which to create the environment. + * @param options - Optional parameters for creating the Python environment. + * @returns A promise that resolves to the created Python environment, or undefined if creation failed. + */ + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; + + /** + * Removes the specified Python environment. + * @param environment - The Python environment to remove. + * @returns A promise that resolves when the environment is removed. + */ + remove?(environment: PythonEnvironment): Promise; + + /** + * Refreshes the list of Python environments within the specified scope. + * @param scope - The scope within which to refresh environments. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + */ + onDidChangeEnvironments?: Event; + + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + * @returns A promise that resolves when the environment is set. + */ + set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + get(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the current Python environment changes. + */ + onDidChangeEnvironment?: Event; + + /** + * Resolves the specified Python environment. The environment can be either a {@link PythonEnvironment} or a {@link Uri} context. + * + * This method is used to obtain a fully detailed {@link PythonEnvironment} object. The input can be: + * - A {@link PythonEnvironment} object, which might be missing key details such as {@link PythonEnvironment.execInfo}. + * - A {@link Uri} object, which typically represents either: + * - A folder that contains the Python environment. + * - The path to a Python executable. + * + * @param context - The context for resolving the environment, which can be a {@link PythonEnvironment} or a {@link Uri}. + * @returns A promise that resolves to the fully detailed {@link PythonEnvironment}, or `undefined` if the environment cannot be resolved. + */ + resolve(context: ResolveEnvironmentContext): Promise; + + /** + * Clears the environment manager's cache. + * + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a package ID. + */ +export interface PackageId { + /** + * The ID of the package. + */ + id: string; + + /** + * The ID of the package manager. + */ + managerId: string; + + /** + * The ID of the environment in which the package is installed. + */ + environmentId: string; +} + +/** + * Interface representing package information. + */ +export interface PackageInfo { + /** + * The name of the package. + */ + readonly name: string; + + /** + * The display name of the package. + */ + readonly displayName: string; + + /** + * The version of the package. + */ + readonly version?: string; + + /** + * The description of the package. + */ + readonly description?: string; + + /** + * The tooltip for the package, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The URIs associated with the package. + */ + readonly uris?: readonly Uri[]; +} + +/** + * Interface representing a package. + */ +export interface Package extends PackageInfo { + /** + * The ID of the package. + */ + readonly pkgId: PackageId; +} + +/** + * Enum representing the kinds of package changes. + */ +export enum PackageChangeKind { + /** + * Indicates that a package was added. + */ + add = 'add', + + /** + * Indicates that a package was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when packages change. + */ +export interface DidChangePackagesEventArgs { + /** + * The Python environment in which the packages changed. + */ + environment: PythonEnvironment; + + /** + * The package manager responsible for the changes. + */ + manager: PackageManager; + + /** + * The list of changes, each containing the kind of change and the package affected. + */ + changes: { kind: PackageChangeKind; pkg: Package }[]; +} + +/** + * Interface representing a package manager. + */ +export interface PackageManager { + /** + * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + name: string; + + /** + * The display name of the package manager. + */ + displayName?: string; + + /** + * The description of the package manager. + */ + description?: string; + + /** + * The tooltip for the package manager, which can be a string or a Markdown string. + */ + tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + iconPath?: IconPath; + + /** + * The log output channel for the package manager. + */ + log?: LogOutputChannel; + + /** + * Installs/Uninstall packages in the specified Python environment. + * @param environment - The Python environment in which to install packages. + * @param options - Options for managing packages. + * @returns A promise that resolves when the installation is complete. + */ + manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; + + /** + * Refreshes the package list for the specified Python environment. + * @param environment - The Python environment for which to refresh the package list. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(environment: PythonEnvironment): Promise; + + /** + * Retrieves the list of packages for the specified Python environment. + * @param environment - The Python environment for which to retrieve packages. + * @returns An array of packages, or undefined if the packages could not be retrieved. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event that is fired when packages change. + */ + onDidChangePackages?: Event; + + /** + * Clears the package manager's cache. + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a Python project. + */ +export interface PythonProject { + /** + * The name of the Python project. + */ + readonly name: string; + + /** + * The URI of the Python project. + */ + readonly uri: Uri; + + /** + * The description of the Python project. + */ + readonly description?: string; + + /** + * The tooltip for the Python project, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python project, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Options for creating a Python project. + */ +export interface PythonProjectCreatorOptions { + /** + * The name of the Python project. + */ + name: string; + + /** + * Path provided as the root for the project. + */ + rootUri: Uri; + + /** + * Boolean indicating whether the project should be created without any user input. + */ + quickCreate?: boolean; +} + +/** + * Interface representing a creator for Python projects. + */ +export interface PythonProjectCreator { + /** + * The name of the Python project creator. + */ + readonly name: string; + + /** + * The display name of the Python project creator. + */ + readonly displayName?: string; + + /** + * The description of the Python project creator. + */ + readonly description?: string; + + /** + * The tooltip for the Python project creator, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python project creator, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Creates a new Python project(s) or, if files are not a project, returns Uri(s) to the created files. + * Anything that needs its own python environment constitutes a project. + * @param options Optional parameters for creating the Python project. + * @returns A promise that resolves to one of the following: + * - PythonProject or PythonProject[]: when a single or multiple projects are created. + * - Uri or Uri[]: when files are created that do not constitute a project. + * - undefined: if project creation fails. + */ + create(options?: PythonProjectCreatorOptions): Promise; + + /** + * A flag indicating whether the project creator supports quick create where no user input is required. + */ + readonly supportsQuickCreate?: boolean; +} + +/** + * Event arguments for when Python projects change. + */ +export interface DidChangePythonProjectsEventArgs { + /** + * The list of Python projects that were added. + */ + added: PythonProject[]; + + /** + * The list of Python projects that were removed. + */ + removed: PythonProject[]; +} + +export type PackageManagementOptions = + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall?: string[]; + } + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install?: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall: string[]; + }; + +/** + * Options for creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. + */ + quickCreate?: boolean; + /** + * Packages to install in addition to the automatically picked packages as a part of creating environment. + */ + additionalPackages?: string[]; +} + +/** + * Object representing the process started using run in background API. + */ +export interface PythonProcess { + /** + * The process ID of the Python process. + */ + readonly pid?: number; + + /** + * The standard input of the Python process. + */ + readonly stdin: NodeJS.WritableStream; + + /** + * The standard output of the Python process. + */ + readonly stdout: NodeJS.ReadableStream; + + /** + * The standard error of the Python process. + */ + readonly stderr: NodeJS.ReadableStream; + + /** + * Kills the Python process. + */ + kill(): void; + + /** + * Event that is fired when the Python process exits. + */ + onExit(listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; +} + +export interface PythonEnvironmentManagerRegistrationApi { + /** + * Register an environment manager implementation. + * + * @param manager Environment Manager implementation to register. + * @returns A disposable that can be used to unregister the environment manager. + * @see {@link EnvironmentManager} + */ + registerEnvironmentManager(manager: EnvironmentManager): Disposable; +} + +export interface PythonEnvironmentItemApi { + /** + * Create a Python environment item from the provided environment info. This item is used to interact + * with the environment. + * + * @param info Some details about the environment like name, version, etc. needed to interact with the environment. + * @param manager The environment manager to associate with the environment. + * @returns The Python environment. + */ + createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment; +} + +export interface PythonEnvironmentManagementApi { + /** + * Create a Python environment using environment manager associated with the scope. + * + * @param scope Where the environment is to be created. + * @param options Optional parameters for creating the Python environment. + * @returns The Python environment created. `undefined` if not created. + */ + createEnvironment( + scope: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise; + + /** + * Remove a Python environment. + * + * @param environment The Python environment to remove. + * @returns A promise that resolves when the environment has been removed. + */ + removeEnvironment(environment: PythonEnvironment): Promise; +} + +export interface PythonEnvironmentsApi { + /** + * Initiates a refresh of Python environments within the specified scope. + * @param scope - The scope within which to search for environments. + * @returns A promise that resolves when the search is complete. + */ + refreshEnvironments(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + * @see {@link DidChangeEnvironmentsEventArgs} + */ + onDidChangeEnvironments: Event; + + /** + * This method is used to get the details missing from a PythonEnvironment. Like + * {@link PythonEnvironment.execInfo} and other details. + * + * @param context : The PythonEnvironment or Uri for which details are required. + */ + resolveEnvironment(context: ResolveEnvironmentContext): Promise; +} + +export interface PythonProjectEnvironmentApi { + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + */ + setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + getEnvironment(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the selected Python environment changes for Project, Folder or File. + * @see {@link DidChangeEnvironmentEventArgs} + */ + onDidChangeEnvironment: Event; +} + +export interface PythonEnvironmentManagerApi + extends PythonEnvironmentManagerRegistrationApi, + PythonEnvironmentItemApi, + PythonEnvironmentManagementApi, + PythonEnvironmentsApi, + PythonProjectEnvironmentApi {} + +export interface PythonPackageManagerRegistrationApi { + /** + * Register a package manager implementation. + * + * @param manager Package Manager implementation to register. + * @returns A disposable that can be used to unregister the package manager. + * @see {@link PackageManager} + */ + registerPackageManager(manager: PackageManager): Disposable; +} + +export interface PythonPackageGetterApi { + /** + * Refresh the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is to be refreshed. + * @returns A promise that resolves when the list of packages has been refreshed. + */ + refreshPackages(environment: PythonEnvironment): Promise; + + /** + * Get the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is required. + * @returns The list of packages in the Python Environment. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event raised when the list of packages in a Python Environment changes. + * @see {@link DidChangePackagesEventArgs} + */ + onDidChangePackages: Event; +} + +export interface PythonPackageItemApi { + /** + * Create a package item from the provided package info. + * + * @param info The package info. + * @param environment The Python Environment in which the package is installed. + * @param manager The package manager that installed the package. + * @returns The package item. + */ + createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package; +} + +export interface PythonPackageManagementApi { + /** + * Install/Uninstall packages into a Python Environment. + * + * @param environment The Python Environment into which packages are to be installed. + * @param packages The packages to install. + * @param options Options for installing packages. + */ + managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; +} + +export interface PythonPackageManagerApi + extends PythonPackageManagerRegistrationApi, + PythonPackageGetterApi, + PythonPackageManagementApi, + PythonPackageItemApi {} + +export interface PythonProjectCreationApi { + /** + * Register a Python project creator. + * + * @param creator The project creator to register. + * @returns A disposable that can be used to unregister the project creator. + * @see {@link PythonProjectCreator} + */ + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; +} +export interface PythonProjectGetterApi { + /** + * Get all python projects. + */ + getPythonProjects(): readonly PythonProject[]; + + /** + * Get the python project for a given URI. + * + * @param uri The URI of the project + * @returns The project or `undefined` if not found. + */ + getPythonProject(uri: Uri): PythonProject | undefined; +} + +export interface PythonProjectModifyApi { + /** + * Add a python project or projects to the list of projects. + * + * @param projects The project or projects to add. + */ + addPythonProject(projects: PythonProject | PythonProject[]): void; + + /** + * Remove a python project from the list of projects. + * + * @param project The project to remove. + */ + removePythonProject(project: PythonProject): void; + + /** + * Event raised when python projects are added or removed. + * @see {@link DidChangePythonProjectsEventArgs} + */ + onDidChangePythonProjects: Event; +} + +/** + * The API for interacting with Python projects. A project in python is any folder or file that is a contained + * in some manner. For example, a PEP-723 compliant file can be treated as a project. A folder with a `pyproject.toml`, + * or just python files can be treated as a project. All this allows you to do is set a python environment for that project. + * + * By default all `vscode.workspace.workspaceFolders` are treated as projects. + */ +export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} + +export interface PythonTerminalCreateOptions extends TerminalOptions { + /** + * Whether to disable activation on create. + */ + disableActivation?: boolean; +} + +export interface PythonTerminalCreateApi { + /** + * Creates a terminal and activates any (activatable) environment for the terminal. + * + * @param environment The Python environment to activate. + * @param options Options for creating the terminal. + * + * Note: Non-activatable environments have no effect on the terminal. + */ + createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; +} + +/** + * Options for running a Python script or module in a terminal. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTerminalExecutionOptions { + /** + * Current working directory for the terminal. This in only used to create the terminal. + */ + cwd: string | Uri; + + /** + * Arguments to pass to the python executable. + */ + args?: string[]; + + /** + * Set `true` to show the terminal. + */ + show?: boolean; +} + +export interface PythonTerminalRunApi { + /** + * Runs a Python script or module in a terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. + * + * Note: + * - If you restart VS Code, this will create a new terminal, this is a limitation of VS Code. + * - If you close the terminal, this will create a new terminal. + * - In cases of multi-root/project scenario, it will create a separate terminal for each project. + */ + runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise; + + /** + * Runs a Python script or module in a dedicated terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. This terminal will be dedicated to the script, + * and selected based on the `terminalKey`. + * + * @param terminalKey A unique key to identify the terminal. For scripts you can use the Uri of the script file. + */ + runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise; +} + +/** + * Options for running a Python task. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTaskExecutionOptions { + /** + * Name of the task to run. + */ + name: string; + + /** + * Arguments to pass to the python executable. + */ + args: string[]; + + /** + * The Python project to use for the task. + */ + project?: PythonProject; + + /** + * Current working directory for the task. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the task. + */ + env?: { [key: string]: string }; +} + +export interface PythonTaskRunApi { + /** + * Run a Python script or module as a task. + * + */ + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise; +} + +/** + * Options for running a Python script or module in the background. + */ +export interface PythonBackgroundRunOptions { + /** + * The Python environment to use for running the script or module. + */ + args: string[]; + + /** + * Current working directory for the script or module. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the script or module. + */ + env?: { [key: string]: string | undefined }; +} +export interface PythonBackgroundRunApi { + /** + * Run a Python script or module in the background. This API will create a new process to run the script or module. + */ + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise; +} + +export interface PythonExecutionApi + extends PythonTerminalCreateApi, + PythonTerminalRunApi, + PythonTaskRunApi, + PythonBackgroundRunApi {} + +/** + * Event arguments for when the monitored `.env` files or any other sources change. + */ +export interface DidChangeEnvironmentVariablesEventArgs { + /** + * The URI of the file that changed. No `Uri` means a non-file source of environment variables changed. + */ + uri?: Uri; + + /** + * The type of change that occurred. + */ + changeTye: FileChangeType; +} + +export interface PythonEnvironmentVariablesApi { + /** + * Get environment variables for a workspace. This picks up `.env` file from the root of the + * workspace. + * + * Order of overrides: + * 1. `baseEnvVar` if given or `process.env` + * 2. `.env` file from the "python.envFile" setting in the workspace. + * 3. `.env` file at the root of the python project. + * 4. `overrides` in the order provided. + * + * @param uri The URI of the project, workspace or a file in a for which environment variables are required. + * @param overrides Additional environment variables to override the defaults. + * @param baseEnvVar The base environment variables that should be used as a starting point. + */ + getEnvironmentVariables( + uri: Uri, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }>; + + /** + * Event raised when `.env` file changes or any other monitored source of env variable changes. + */ + onDidChangeEnvironmentVariables: Event; +} + +/** + * The API for interacting with Python environments, package managers, and projects. + */ +export interface PythonEnvironmentApi + extends PythonEnvironmentManagerApi, + PythonPackageManagerApi, + PythonProjectApi, + PythonExecutionApi, + PythonEnvironmentVariablesApi {} diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts new file mode 100644 index 000000000000..ecd8eef21845 --- /dev/null +++ b/src/client/environmentApi.ts @@ -0,0 +1,444 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, EventEmitter, Uri, workspace, WorkspaceFolder } from 'vscode'; +import * as pathUtils from 'path'; +import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; +import { Architecture } from './common/utils/platform'; +import { IServiceContainer } from './ioc/types'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; +import { getEnvPath } from './pythonEnvironments/base/info/env'; +import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; +import { IPythonExecutionFactory } from './common/process/types'; +import { traceError, traceInfo, traceVerbose } from './logging'; +import { isParentPath, normCasePath } from './common/platform/fs-paths'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; +import { reportActiveInterpreterChangedDeprecated, reportInterpretersChanged } from './deprecatedProposedApi'; +import { IEnvironmentVariablesProvider } from './common/variables/types'; +import { getWorkspaceFolder, getWorkspaceFolders } from './common/vscodeApis/workspaceApis'; +import { + ActiveEnvironmentPathChangeEvent, + Environment, + EnvironmentPath, + EnvironmentsChangeEvent, + EnvironmentTools, + EnvironmentType, + EnvironmentVariablesChangeEvent, + PythonExtension, + RefreshOptions, + ResolvedEnvironment, + Resource, +} from './api/types'; +import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; +import { EnvironmentKnownCache } from './environmentKnownCache'; +import type { JupyterPythonEnvironmentApi } from './jupyter/jupyterIntegration'; +import { noop } from './common/utils/misc'; + +type ActiveEnvironmentChangeEvent = { + resource: WorkspaceFolder | undefined; + path: string; +}; + +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const previousEnvMap = new Map(); +export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { + const oldPath = previousEnvMap.get(e.resource?.uri.fsPath ?? ''); + if (oldPath === e.path) { + return; + } + previousEnvMap.set(e.resource?.uri.fsPath ?? '', e.path); + onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); + reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); +} + +const onEnvironmentsChanged = new EventEmitter(); +const onEnvironmentVariablesChanged = new EventEmitter(); +const environmentsReference = new Map(); + +/** + * Make all properties in T mutable. + */ +type Mutable = { + -readonly [P in keyof T]: Mutable; +}; + +export class EnvironmentReference implements Environment { + readonly id: string; + + constructor(public internal: Environment) { + this.id = internal.id; + } + + get executable() { + return Object.freeze(this.internal.executable); + } + + get environment() { + return Object.freeze(this.internal.environment); + } + + get version() { + return Object.freeze(this.internal.version); + } + + get tools() { + return Object.freeze(this.internal.tools); + } + + get path() { + return Object.freeze(this.internal.path); + } + + updateEnv(newInternal: Environment) { + this.internal = newInternal; + } +} + +function getEnvReference(e: Environment) { + let envClass = environmentsReference.get(e.id); + if (!envClass) { + envClass = new EnvironmentReference(e); + } else { + envClass.updateEnv(e); + } + environmentsReference.set(e.id, envClass); + return envClass; +} + +function filterUsingVSCodeContext(e: PythonEnvInfo) { + const folders = getWorkspaceFolders(); + if (e.searchLocation) { + // Only return local environments that are in the currently opened workspace folders. + const envFolderUri = e.searchLocation; + if (folders) { + return folders.some((folder) => isParentPath(envFolderUri.fsPath, folder.uri.fsPath)); + } + return false; + } + return true; +} + +export function buildEnvironmentApi( + discoveryApi: IDiscoveryAPI, + serviceContainer: IServiceContainer, + jupyterPythonEnvsApi: JupyterPythonEnvironmentApi, +): PythonExtension['environments'] { + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const configService = serviceContainer.get(IConfigurationService); + const disposables = serviceContainer.get(IDisposableRegistry); + const extensions = serviceContainer.get(IExtensions); + const envVarsProvider = serviceContainer.get(IEnvironmentVariablesProvider); + let knownCache: EnvironmentKnownCache; + + function initKnownCache() { + const knownEnvs = discoveryApi + .getEnvs() + .filter((e) => filterUsingVSCodeContext(e)) + .map((e) => updateReference(e)); + return new EnvironmentKnownCache(knownEnvs); + } + function sendApiTelemetry(apiName: string, args?: unknown) { + extensions + .determineExtensionFromCallStack() + .then((info) => { + const p = Math.random(); + if (p <= 0.001) { + // Only send API telemetry 1% of the time, as it can be chatty. + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + }); + } + traceVerbose(`Extension ${info.extensionId} accessed ${apiName} with args: ${JSON.stringify(args)}`); + }) + .ignoreErrors(); + } + + function getActiveEnvironmentPath(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; + const jupyterEnv = + resource && jupyterPythonEnvsApi.getPythonEnvironment + ? jupyterPythonEnvsApi.getPythonEnvironment(resource) + : undefined; + if (jupyterEnv) { + traceVerbose('Python Environment returned from Jupyter', resource?.fsPath, jupyterEnv.id); + return { + id: jupyterEnv.id, + path: jupyterEnv.path, + }; + } + const path = configService.getSettings(resource).pythonPath; + const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); + return { + id, + path, + }; + } + + disposables.push( + onDidActiveInterpreterChangedEvent.event((e) => { + let scope = 'global'; + if (e.resource) { + scope = e.resource instanceof Uri ? e.resource.fsPath : e.resource.uri.fsPath; + } + traceInfo(`Active interpreter [${scope}]: `, e.path); + }), + discoveryApi.onProgress((e) => { + if (e.stage === ProgressReportStage.discoveryFinished) { + knownCache = initKnownCache(); + } + }), + discoveryApi.onChanged((e) => { + const env = e.new ?? e.old; + if (!env || !filterUsingVSCodeContext(env)) { + // Filter out environments that are not in the current workspace. + return; + } + if (!knownCache) { + knownCache = initKnownCache(); + } + if (e.old) { + if (e.new) { + const newEnv = updateReference(e.new); + knownCache.updateEnv(convertEnvInfo(e.old), newEnv); + traceVerbose('Python API env change detected', env.id, 'update'); + onEnvironmentsChanged.fire({ type: 'update', env: newEnv }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'update', + }, + ]); + } else { + const oldEnv = updateReference(e.old); + knownCache.updateEnv(oldEnv, undefined); + traceVerbose('Python API env change detected', env.id, 'remove'); + onEnvironmentsChanged.fire({ type: 'remove', env: oldEnv }); + reportInterpretersChanged([ + { + path: getEnvPath(e.old.executable.filename, e.old.location).path, + type: 'remove', + }, + ]); + } + } else if (e.new) { + const newEnv = updateReference(e.new); + knownCache.addEnv(newEnv); + traceVerbose('Python API env change detected', env.id, 'add'); + onEnvironmentsChanged.fire({ type: 'add', env: newEnv }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'add', + }, + ]); + } + }), + envVarsProvider.onDidEnvironmentVariablesChange((e) => { + onEnvironmentVariablesChanged.fire({ + resource: getWorkspaceFolder(e), + env: envVarsProvider.getEnvironmentVariablesSync(e), + }); + }), + onEnvironmentsChanged, + onEnvironmentVariablesChanged, + jupyterPythonEnvsApi.onDidChangePythonEnvironment + ? jupyterPythonEnvsApi.onDidChangePythonEnvironment((e) => { + const jupyterEnv = getActiveEnvironmentPath(e); + onDidActiveInterpreterChangedEvent.fire({ + id: jupyterEnv.id, + path: jupyterEnv.path, + resource: e, + }); + }, undefined) + : { dispose: noop }, + ); + if (!knownCache!) { + knownCache = initKnownCache(); + } + + const environmentApi: PythonExtension['environments'] = { + getEnvironmentVariables: (resource?: Resource) => { + sendApiTelemetry('getEnvironmentVariables'); + resource = resource && 'uri' in resource ? resource.uri : resource; + return envVarsProvider.getEnvironmentVariablesSync(resource); + }, + get onDidEnvironmentVariablesChange() { + sendApiTelemetry('onDidEnvironmentVariablesChange'); + return onEnvironmentVariablesChanged.event; + }, + getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentPath'); + return getActiveEnvironmentPath(resource); + }, + updateActiveEnvironmentPath(env: Environment | EnvironmentPath | string, resource?: Resource): Promise { + sendApiTelemetry('updateActiveEnvironmentPath'); + const path = typeof env !== 'string' ? env.path : env; + resource = resource && 'uri' in resource ? resource.uri : resource; + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + get onDidChangeActiveEnvironmentPath() { + sendApiTelemetry('onDidChangeActiveEnvironmentPath'); + return onDidActiveInterpreterChangedEvent.event; + }, + resolveEnvironment: async (env: Environment | EnvironmentPath | string) => { + if (!workspace.isTrusted) { + throw new Error('Not allowed to resolve environment in an untrusted workspace'); + } + let path = typeof env !== 'string' ? env.path : env; + if (pathUtils.basename(path) === path) { + // Value can be `python`, `python3`, `python3.9` etc. + // This case could eventually be handled by the internal discovery API itself. + const pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); + const pythonExecutionService = await pythonExecutionFactory.create({ pythonPath: path }); + const fullyQualifiedPath = await pythonExecutionService.getExecutablePath().catch((ex) => { + traceError('Cannot resolve full path', ex); + return undefined; + }); + // Python path is invalid or python isn't installed. + if (!fullyQualifiedPath) { + return undefined; + } + path = fullyQualifiedPath; + } + sendApiTelemetry('resolveEnvironment', env); + return resolveEnvironment(path, discoveryApi); + }, + get known(): Environment[] { + // Do not send telemetry for "known", as this may be called 1000s of times so it can significant: + // sendApiTelemetry('known'); + return knownCache.envs; + }, + async refreshEnvironments(options?: RefreshOptions) { + if (!workspace.isTrusted) { + traceError('Not allowed to refresh environments in an untrusted workspace'); + return; + } + await discoveryApi.triggerRefresh(undefined, { + ifNotTriggerredAlready: !options?.forceRefresh, + }); + sendApiTelemetry('refreshEnvironments'); + }, + get onDidChangeEnvironments() { + sendApiTelemetry('onDidChangeEnvironments'); + return onEnvironmentsChanged.event; + }, + ...buildEnvironmentCreationApi(), + }; + return environmentApi; +} + +async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Promise { + const env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + const resolvedEnv = getEnvReference(convertCompleteEnvInfo(env)) as ResolvedEnvironment; + if (resolvedEnv.version?.major === -1 || resolvedEnv.version?.minor === -1 || resolvedEnv.version?.micro === -1) { + traceError(`Invalid version for ${path}: ${JSON.stringify(env)}`); + } + return resolvedEnv; +} + +export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { + const version = { ...env.version, sysVersion: env.version.sysVersion }; + let tool = convertKind(env.kind); + if (env.type && !tool) { + tool = 'Unknown'; + } + const { path } = getEnvPath(env.executable.filename, env.location); + const resolvedEnv: ResolvedEnvironment = { + path, + id: env.id!, + executable: { + uri: env.executable.filename === 'python' ? undefined : Uri.file(env.executable.filename), + bitness: convertBitness(env.arch), + sysPrefix: env.executable.sysPrefix, + }, + environment: env.type + ? { + type: convertEnvType(env.type), + name: env.name === '' ? undefined : env.name, + folderUri: Uri.file(env.location), + workspaceFolder: getWorkspaceFolder(env.searchLocation), + } + : undefined, + version: env.executable.filename === 'python' ? undefined : (version as ResolvedEnvironment['version']), + tools: tool ? [tool] : [], + }; + return resolvedEnv; +} + +function convertEnvType(envType: PythonEnvType): EnvironmentType { + if (envType === PythonEnvType.Conda) { + return 'Conda'; + } + if (envType === PythonEnvType.Virtual) { + return 'VirtualEnvironment'; + } + return 'Unknown'; +} + +function convertKind(kind: PythonEnvKind): EnvironmentTools | undefined { + switch (kind) { + case PythonEnvKind.Venv: + return 'Venv'; + case PythonEnvKind.Pipenv: + return 'Pipenv'; + case PythonEnvKind.Poetry: + return 'Poetry'; + case PythonEnvKind.Hatch: + return 'Hatch'; + case PythonEnvKind.VirtualEnvWrapper: + return 'VirtualEnvWrapper'; + case PythonEnvKind.VirtualEnv: + return 'VirtualEnv'; + case PythonEnvKind.Conda: + return 'Conda'; + case PythonEnvKind.Pyenv: + return 'Pyenv'; + default: + return undefined; + } +} + +export function convertEnvInfo(env: PythonEnvInfo): Environment { + const convertedEnv = convertCompleteEnvInfo(env) as Mutable; + if (convertedEnv.executable.sysPrefix === '') { + convertedEnv.executable.sysPrefix = undefined; + } + if (convertedEnv.version?.sysVersion === '') { + convertedEnv.version.sysVersion = undefined; + } + if (convertedEnv.version?.major === -1) { + convertedEnv.version.major = undefined; + } + if (convertedEnv.version?.micro === -1) { + convertedEnv.version.micro = undefined; + } + if (convertedEnv.version?.minor === -1) { + convertedEnv.version.minor = undefined; + } + return convertedEnv as Environment; +} + +function updateReference(env: PythonEnvInfo): Environment { + return getEnvReference(convertEnvInfo(env)); +} + +function convertBitness(arch: Architecture) { + switch (arch) { + case Architecture.x64: + return '64-bit'; + case Architecture.x86: + return '32-bit'; + default: + return 'Unknown'; + } +} + +function getEnvID(path: string) { + return normCasePath(path); +} diff --git a/src/client/environmentKnownCache.ts b/src/client/environmentKnownCache.ts new file mode 100644 index 000000000000..287f5bab343f --- /dev/null +++ b/src/client/environmentKnownCache.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Environment } from './api/types'; + +/** + * Workaround temp cache until types are consolidated. + */ +export class EnvironmentKnownCache { + private _envs: Environment[] = []; + + constructor(envs: Environment[]) { + this._envs = envs; + } + + public get envs(): Environment[] { + return this._envs; + } + + public addEnv(env: Environment): void { + const found = this._envs.find((e) => env.id === e.id); + if (!found) { + this._envs.push(env); + } + } + + public updateEnv(oldValue: Environment, newValue: Environment | undefined): void { + const index = this._envs.findIndex((e) => oldValue.id === e.id); + if (index !== -1) { + if (newValue === undefined) { + this._envs.splice(index, 1); + } else { + this._envs[index] = newValue; + } + } + } +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 2abbf7d8658e..c3fb2a3ab3b0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,44 +1,52 @@ 'use strict'; -// 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 = {}; +const durations = {} as IStartupDurations; import { StopWatch } from './common/utils/stopWatch'; // Do not move this line of code (used to measure extension load times). const stopWatch = new StopWatch(); +// Initialize file logging here. This should not depend on too many things. +import { initializeFileLogging, traceError } from './logging'; +const logDispose: { dispose: () => void }[] = []; +initializeFileLogging(logDispose); + //=============================================== // 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 { buildApi } from './api'; +import { IApplicationShell, IWorkspaceService } from './common/application/types'; +import { IDisposableRegistry, IExperimentService, 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 { activateComponents, activateFeatures } from './extensionActivation'; +import { initializeStandard, initializeComponents, initializeGlobals } from './extensionInit'; import { IServiceContainer } from './ioc/types'; import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; +import { IStartupDurations } from './types'; +import { runAfterActivation } from './common/utils/runAfterActivation'; +import { IInterpreterService } from './interpreter/contracts'; +import { PythonExtension } from './api/types'; +import { WorkspaceService } from './common/application/workspace'; +import { disposeAll } from './common/utils/resourceLifecycle'; +import { ProposedExtensionAPI } from './proposedApiTypes'; +import { buildProposedApi } from './proposedApi'; +import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState'; +import { registerTools } from './chat'; +import { IRecommendedEnvironmentService } from './interpreter/configuration/types'; +import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; +import { registerTestCommands } from './testing/main'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -51,83 +59,139 @@ let activatedServiceContainer: IServiceContainer | undefined; ///////////////////////////// // public functions -export async function activate(context: IExtensionContext): Promise { - let api: IExtensionApi; +export async function activate(context: IExtensionContext): Promise { + let api: PythonExtension; let ready: Promise; let serviceContainer: IServiceContainer; + let isFirstSession: boolean | undefined; try { + isFirstSession = context.globalState.get(GLOBAL_PERSISTENT_KEYS, []).length === 0; + const workspaceService = new WorkspaceService(); + context.subscriptions.push( + workspaceService.onDidGrantWorkspaceTrust(async () => { + await deactivate(); + await activate(context); + }), + ); [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); + await handleError(ex as Error, durations); throw ex; // re-raise } // Send the "success" telemetry only if activation did not fail. // Otherwise Telemetry is send via the error handler. - sendStartupTelemetry(ready, durations, stopWatch, serviceContainer) + sendStartupTelemetry(ready, durations, stopWatch, serviceContainer, isFirstSession) // Run in the background. .ignoreErrors(); return api; } -export function deactivate(): Thenable { +export async function deactivate(): Promise { // Make sure to shutdown anybody who needs it. if (activatedServiceContainer) { - const registry = activatedServiceContainer.get(IAsyncDisposableRegistry); - if (registry) { - return registry.dispose(); - } + const disposables = activatedServiceContainer.get(IDisposableRegistry); + await disposeAll(disposables); + // Remove everything that is already disposed. + while (disposables.pop()); } - - return Promise.resolve(); } ///////////////////////////// // activation helpers -// tslint:disable-next-line:max-func-body-length async function activateUnsafe( context: IExtensionContext, startupStopWatch: StopWatch, - startupDurations: Record -): Promise<[IExtensionApi, Promise, IServiceContainer]> { + startupDurations: IStartupDurations, +): Promise<[PythonExtension & ProposedExtensionAPI, Promise, IServiceContainer]> { + // Add anything that we got from initializing logs to dispose. + context.subscriptions.push(...logDispose); + const activationDeferred = createDeferred(); displayProgress(activationDeferred.promise); startupDurations.startActivateTime = startupStopWatch.elapsedTime; + const activationStopWatch = new StopWatch(); //=============================================== // activation starts here - const [serviceManager, serviceContainer] = initializeGlobals(context); - activatedServiceContainer = serviceContainer; - initializeComponents(context, serviceManager, serviceContainer); - const { activationPromise } = await activateComponents(context, serviceManager, serviceContainer); + // First we initialize. + const ext = initializeGlobals(context); + activatedServiceContainer = ext.legacyIOC.serviceContainer; + // Note standard utils especially experiment and platform code are fundamental to the extension + // and should be available before we activate anything else.Hence register them first. + initializeStandard(ext); + + // Register test services and commands early to prevent race conditions. + unitTestsRegisterTypes(ext.legacyIOC.serviceManager); + registerTestCommands(activatedServiceContainer); + + // We need to activate experiments before initializing components as objects are created or not created based on experiments. + const experimentService = activatedServiceContainer.get(IExperimentService); + // This guarantees that all experiment information has loaded & all telemetry will contain experiment info. + await experimentService.activate(); + const components = await initializeComponents(ext); + + // Then we finish activating. + const componentsActivated = await activateComponents(ext, components, activationStopWatch); + activateFeatures(ext, components); + + const nonBlocking = componentsActivated.map((r) => r.fullyReady); + const activationPromise = (async () => { + await Promise.all(nonBlocking); + })(); //=============================================== // activation ends here - startupDurations.endActivateTime = startupStopWatch.elapsedTime; + startupDurations.totalActivateTime = startupStopWatch.elapsedTime - startupDurations.startActivateTime; activationDeferred.resolve(); - const api = buildApi(activationPromise, serviceManager, serviceContainer); - return [api, activationPromise, serviceContainer]; + setTimeout(async () => { + if (activatedServiceContainer) { + const workspaceService = activatedServiceContainer.get(IWorkspaceService); + if (workspaceService.isTrusted) { + const interpreterManager = activatedServiceContainer.get(IInterpreterService); + const workspaces = workspaceService.workspaceFolders ?? []; + await interpreterManager + .refresh(workspaces.length > 0 ? workspaces[0].uri : undefined) + .catch((ex) => traceError('Python Extension: interpreterManager.refresh', ex)); + } + } + + runAfterActivation(); + }); + + const api = buildApi( + activationPromise, + ext.legacyIOC.serviceManager, + ext.legacyIOC.serviceContainer, + components.pythonEnvs, + ); + const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer); + registerTools(context, components.pythonEnvs, api.environments, ext.legacyIOC.serviceContainer); + ext.legacyIOC.serviceContainer + .get(IRecommendedEnvironmentService) + .registerEnvApi(api.environments); + return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer]; } -// tslint:disable-next-line:no-any function displayProgress(promise: Promise) { - const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension() }; + const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension }; window.withProgress(progressOptions, () => promise); } ///////////////////////////// // error handling -async function handleError(ex: Error, startupDurations: Record) { +async function handleError(ex: Error, startupDurations: IStartupDurations) { notifyUser( - "Extension activation failed, run the 'Developer: Toggle Developer Tools' command for more information." + "Extension activation failed, run the 'Developer: Toggle Developer Tools' command for more information.", ); traceError('extension activation failed', ex); + await sendErrorTelemetry(ex, startupDurations, activatedServiceContainer); } @@ -137,14 +201,12 @@ interface IAppShell { 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); + traceError('Failed to Notify User', ex); } } diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index b842440db208..57bcb8237eeb 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -3,220 +3,199 @@ 'use strict'; -// tslint:disable:max-func-body-length - -import { CodeActionKind, debug, DebugConfigurationProvider, languages, OutputChannel, window } from 'vscode'; +import { DebugConfigurationProvider, debug, languages, window } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; -import { IExtensionActivationManager, ILanguageServerExtension } from './activation/types'; +import { IExtensionActivationManager } 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 { Commands, PYTHON_LANGUAGE, 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 { 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 { IFileSystem } from './common/platform/types'; +import { IConfigurationService, IDisposableRegistry, IExtensions, ILogOutputChannel, IPathUtils } from './common/types'; 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 { IDebugConfigurationService } from './debugger/extension/types'; +import { IInterpreterService } from './interpreter/contracts'; 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 tensorBoardRegisterTypes } from './tensorBoard/serviceRegistry'; 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'; +import { ICodeExecutionHelper, ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; + +// components +import * as pythonEnvironments from './pythonEnvironments'; + +import { ActivationResult, ExtensionState } from './components'; +import { Components } from './extensionInit'; +import { setDefaultLanguageServer } from './activation/common/defaultlanguageServer'; +import { DebugService } from './common/application/debugService'; +import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; +import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; +import { WorkspaceService } from './common/application/workspace'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from './interpreter/configuration/types'; +import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; +import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; +import { initializePersistentStateForTriggers } from './common/persistentState'; +import { DebuggerTypeName } from './debugger/constants'; +import { StopWatch } from './common/utils/stopWatch'; +import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeReplCommand } from './repl/replCommands'; +import { registerTriggerForTerminalREPL } from './terminals/codeExecution/terminalReplWatcher'; +import { registerPythonStartup } from './terminals/pythonStartup'; +import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; +import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; export async function activateComponents( - context: IExtensionContext, - serviceManager: IServiceManager, - serviceContainer: IServiceContainer -) { - // We will be pulling code over from activateLegacy(). + // `ext` is passed to any extra activation funcs. + ext: ExtensionState, + components: Components, + startupStopWatch: StopWatch, +): Promise { + // Note that each activation returns a promise that resolves + // when that activation completes. However, it might have started + // some non-critical background operations that do not block + // extension activation but do block use of the extension "API". + // Each component activation can't just resolve an "inner" promise + // for those non-critical operations because `await` (and + // `Promise.all()`, etc.) will flatten nested promises. Thus + // activation resolves `ActivationResult`, which can safely wrap + // the "inner" promise. + + // TODO: As of now activateLegacy() registers various classes which might + // be required while activating components. Once registration from + // activateLegacy() are moved before we activate other components, we can + // activate them in parallel with the other components. + // https://github.com/microsoft/vscode-python/issues/15380 + // These will go away eventually once everything is refactored into components. + const legacyActivationResult = await activateLegacy(ext, startupStopWatch); + const workspaceService = new WorkspaceService(); + if (!workspaceService.isTrusted) { + return [legacyActivationResult]; + } + const promises: Promise[] = [ + // More component activations will go here + pythonEnvironments.activate(components.pythonEnvs, ext), + ]; + return Promise.all([legacyActivationResult, ...promises]); +} - return activateLegacy(context, serviceManager, serviceContainer); +export function activateFeatures(ext: ExtensionState, _components: Components): void { + const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get( + IInterpreterQuickPick, + ); + const interpreterService: IInterpreterService = ext.legacyIOC.serviceContainer.get( + IInterpreterService, + ); + const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); + registerPixiFeatures(ext.disposables); + registerAllCreateEnvironmentFeatures( + ext.disposables, + interpreterQuickPick, + ext.legacyIOC.serviceContainer.get(IPythonPathUpdaterServiceManager), + interpreterService, + pathUtils, + ); + const executionHelper = ext.legacyIOC.serviceContainer.get(ICodeExecutionHelper); + const commandManager = ext.legacyIOC.serviceContainer.get(ICommandManager); + registerTriggerForTerminalREPL(ext.disposables); + registerStartNativeReplCommand(ext.disposables, interpreterService); + registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager); + registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager); + registerCustomTerminalLinkProvider(ext.disposables); } -///////////////////////////// +/// ////////////////////////// // 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" +async function activateLegacy(ext: ExtensionState, startupStopWatch: StopWatch): Promise { + const { legacyIOC } = ext; + const { serviceManager, serviceContainer } = legacyIOC; - 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); + // register "services" - // 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; + const { enableProposedApi } = applicationEnv.packageJson; serviceManager.addSingletonInstance(UseProposedApi, enableProposedApi); // Feature specific registrations. - variableRegisterTypes(serviceManager); - unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); - interpretersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); debugConfigurationRegisterTypes(serviceManager); + tensorBoardRegisterTypes(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 extensions = serviceContainer.get(IExtensions); + await setDefaultLanguageServer(extensions, serviceManager); - 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; + // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. + serviceContainer.get(IConfigurationService).getSettings().register(); // Language feature registrations. - appRegisterTypes(serviceManager, languageServerType); + appRegisterTypes(serviceManager); providersRegisterTypes(serviceManager); - activationRegisterTypes(serviceManager, languageServerType); + activationRegisterTypes(serviceManager); // "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 workspaceService = serviceContainer.get(IWorkspaceService); 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(); + languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); + if (workspaceService.isTrusted) { + const interpreterManager = serviceContainer.get(IInterpreterService); + interpreterManager.initialize(); + if (!workspaceService.isVirtualWorkspace) { + const handlers = serviceManager.getAll(IDebugSessionEventHandlers); + const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); + dispatcher.registerEventHandlers(); + const outputChannel = serviceManager.get(ILogOutputChannel); + disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); + cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); - activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); + serviceContainer.get(IApplicationDiagnostics).register(); - const sortImports = serviceContainer.get(ISortImportsEditingProvider); - sortImports.registerCommands(); + serviceManager.get(ITerminalAutoActivation).register(); - serviceManager.get(ICodeExecutionManager).registerCommands(); + await registerPythonStartup(ext.context); - const workspaceService = serviceContainer.get(IWorkspaceService); - interpreterManager - .refresh(workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined) - .catch((ex) => traceError('Python Extension: interpreterManager.refresh', ex)); + serviceManager.get(ICodeExecutionManager).registerCommands(); - // Activate data science features - const dataScience = serviceManager.get(IDataScience); - dataScience.activate().ignoreErrors(); + disposables.push(new ReplProvider(serviceContainer)); - context.subscriptions.push(new LinterCommands(serviceManager)); + const terminalProvider = new TerminalProvider(serviceContainer); + terminalProvider.initialize(window.activeTerminal).ignoreErrors(); - languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); + serviceContainer + .getAll(IDebugConfigurationService) + .forEach((debugConfigProvider) => { + disposables.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); + }); + disposables.push(terminalProvider); - 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)); + registerCreateEnvironmentTriggers(disposables); + initializePersistentStateForTriggers(ext.context); + } } - 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] - }) - ); + // "activate" everything else - serviceContainer.getAll(IDebugConfigurationService).forEach((debugConfigProvider) => { - context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); - }); + const manager = serviceContainer.get(IExtensionActivationManager); + disposables.push(manager); - serviceContainer.get(IDebuggerBanner).initialize(); + const activationPromise = manager.activate(startupStopWatch); - return { activationPromise }; + return { fullyReady: activationPromise }; } diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts index 833828fe2350..b161643d2d97 100644 --- a/src/client/extensionInit.ts +++ b/src/client/extensionInit.ts @@ -3,43 +3,98 @@ '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 { Disposable, Memento, window } from 'vscode'; +import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; +import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; +import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; +import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; +import { + GLOBAL_MEMENTO, + IDisposableRegistry, + IExtensionContext, + IMemento, + ILogOutputChannel, + WORKSPACE_MEMENTO, +} from './common/types'; +import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; +import { OutputChannelNames } from './common/utils/localize'; +import { ExtensionState } from './components'; import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; import { IServiceContainer, IServiceManager } from './ioc/types'; -import { registerForIOC } from './pythonEnvironments/legacyIOC'; +import * as pythonEnvironments from './pythonEnvironments'; +import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { registerLogger } from './logging'; +import { OutputChannelLogger } from './logging/outputChannelLogger'; // 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(); +export function initializeGlobals( + // This is stored in ExtensionState. + context: IExtensionContext, +): ExtensionState { + const disposables: IDisposableRegistry = context.subscriptions; + const cont = new Container({ skipBaseClassChecks: true }); 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(IDisposableRegistry, disposables); serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); serviceManager.addSingletonInstance(IExtensionContext, context); - return [serviceManager, serviceContainer]; + const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python, { log: true }); + disposables.push(standardOutputChannel); + disposables.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); + + serviceManager.addSingletonInstance(ILogOutputChannel, standardOutputChannel); + + return { + context, + disposables, + legacyIOC: { serviceManager, serviceContainer }, + }; +} + +/** + * Registers standard utils like experiment and platform code which are fundamental to the extension. + */ +export function initializeStandard(ext: ExtensionState): void { + const { serviceManager } = ext.legacyIOC; + // Core registrations (non-feature specific). + commonRegisterTypes(serviceManager); + variableRegisterTypes(serviceManager); + platformRegisterTypes(serviceManager); + processRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); + + // We will be pulling other code over from activateLegacy(). } -export function initializeComponents( - _context: IExtensionContext, - _serviceManager: IServiceManager, - _serviceContainer: IServiceContainer -) { - registerForIOC(_serviceManager); - // We will be pulling code over from activateLegacy(). +/** + * The set of public APIs from initialized components. + */ +export type Components = { + pythonEnvs: IDiscoveryAPI; +}; + +/** + * Initialize all components in the extension. + */ +export async function initializeComponents(ext: ExtensionState): Promise { + const pythonEnvs = await pythonEnvironments.initialize(ext); + + // Other component initializers go here. + // We will be factoring them out of activateLegacy(). + + return { + pythonEnvs, + }; } diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts deleted file mode 100644 index e086e37f549b..000000000000 --- a/src/client/formatters/autoPep8Formatter.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as vscode from 'vscode'; -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 { - constructor(serviceContainer: IServiceContainer) { - super('autopep8', Product.autopep8, serviceContainer); - } - - 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; - } -} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts deleted file mode 100644 index 435a5503337c..000000000000 --- a/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,148 +0,0 @@ -import * as path from 'path'; -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 { - protected readonly outputChannel: vscode.OutputChannel; - protected readonly workspace: IWorkspaceService; - private readonly helper: IFormatterHelper; - - 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); - } - - 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; - } - - // 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 []; - } - - 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 deleted file mode 100644 index 4c13ef1acbc3..000000000000 --- a/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index b82496a5f1b0..000000000000 --- a/src/client/formatters/dummyFormatter.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 3c09be283815..000000000000 --- a/src/client/formatters/helper.ts +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index e6a3bb6f1728..000000000000 --- a/src/client/formatters/lineFormatter.ts +++ /dev/null @@ -1,484 +0,0 @@ -// 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 deleted file mode 100644 index 196e6c806b5f..000000000000 --- a/src/client/formatters/serviceRegistry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 deleted file mode 100644 index 7f4bcf5b7524..000000000000 --- a/src/client/formatters/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 8bf0d3ad413b..000000000000 --- a/src/client/formatters/yapfFormatter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as vscode from 'vscode'; -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 { - constructor(serviceContainer: IServiceContainer) { - super('yapf', Product.yapf, serviceContainer); - } - - 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; - } -} diff --git a/src/client/interpreter/activation/preWarmVariables.ts b/src/client/interpreter/activation/preWarmVariables.ts deleted file mode 100644 index 1532b152eaab..000000000000 --- a/src/client/interpreter/activation/preWarmVariables.ts +++ /dev/null @@ -1,24 +0,0 @@ -// 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 index 3d32cf84ca61..f47575cad60b 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -1,13 +1,16 @@ +/* eslint-disable max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; + import '../../common/extensions'; +import * as path from 'path'; 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'; @@ -16,28 +19,44 @@ 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 { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; import { IEnvironmentActivationService } from './types'; +import { TraceOptions } from '../../logging/types'; +import { + traceDecoratorError, + traceDecoratorVerbose, + traceError, + traceInfo, + traceVerbose, + traceWarn, +} from '../../logging'; +import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { cache } from '../../common/utils/decorators'; +import { getRunPixiPythonCommand } from '../../pythonEnvironments/common/environmentManagers/pixi'; -const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; -const cacheDuration = 10 * 60 * 1000; -export const getEnvironmentTimeout = 30000; +const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; +const CACHE_DURATION = 10 * 60 * 1000; +const ENVIRONMENT_TIMEOUT = 30000; +const CONDA_ENVIRONMENT_TIMEOUT = 60_000; // The shell under which we'll execute activation scripts. -const defaultShells = { +export 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 + [OSType.Unknown]: undefined, }; const condaRetryMessages = [ 'The process cannot access the file because it is being used by another process', - 'The directory is not empty' + 'The directory is not empty', ]; /** @@ -48,15 +67,19 @@ const condaRetryMessages = [ */ export class EnvironmentActivationServiceCache { private static useStatic = false; + private static staticMap = new Map>(); + private normalMap = new Map>(); - public static forceUseStatic() { + public static forceUseStatic(): void { EnvironmentActivationServiceCache.useStatic = true; } - public static forceUseNormal() { + + public static forceUseNormal(): void { EnvironmentActivationServiceCache.useStatic = false; } + public get(key: string): InMemoryCache | undefined { if (EnvironmentActivationServiceCache.useStatic) { return EnvironmentActivationServiceCache.staticMap.get(key); @@ -64,7 +87,7 @@ export class EnvironmentActivationServiceCache { return this.normalMap.get(key); } - public set(key: string, value: InMemoryCache) { + public set(key: string, value: InMemoryCache): void { if (EnvironmentActivationServiceCache.useStatic) { EnvironmentActivationServiceCache.staticMap.set(key, value); } else { @@ -72,7 +95,7 @@ export class EnvironmentActivationServiceCache { } } - public delete(key: string) { + public delete(key: string): void { if (EnvironmentActivationServiceCache.useStatic) { EnvironmentActivationServiceCache.staticMap.delete(key); } else { @@ -80,7 +103,7 @@ export class EnvironmentActivationServiceCache { } } - public clear() { + public clear(): void { // Don't clear during a test as the environment isn't going to change if (!EnvironmentActivationServiceCache.useStatic) { this.normalMap.clear(); @@ -91,7 +114,9 @@ export class EnvironmentActivationServiceCache { @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, @@ -99,91 +124,182 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi @inject(ICurrentProcess) private currentProcess: ICurrentProcess, @inject(IWorkspaceService) private workspace: IWorkspaceService, @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, ) { this.envVarsService.onDidEnvironmentVariablesChange( () => this.activatedEnvVariablesCache.clear(), this, - this.disposables - ); - - this.interpreterService.onDidChangeInterpreter( - () => this.activatedEnvVariablesCache.clear(), - this, - this.disposables + 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) + + @traceDecoratorVerbose('getActivatedEnvironmentVariables', TraceOptions.Arguments) public async getActivatedEnvironmentVariables( resource: Resource, - interpreter?: PythonInterpreter, - allowExceptions?: boolean + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, ): Promise { + const stopWatch = new StopWatch(); // Cache key = resource + interpreter. const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource); + interpreter = interpreter ?? (await this.interpreterService.getActiveInterpreter(resource)); const interpreterPath = this.platform.isWindows ? interpreter?.path.toLowerCase() : interpreter?.path; - const cacheKey = `${workspaceKey}_${interpreterPath}`; + const cacheKey = `${workspaceKey}_${interpreterPath}_${shell}`; 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; - }); + const memCache = new InMemoryCache(CACHE_DURATION); + return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions, shell) + .then((vars) => { + memCache.data = vars; + this.activatedEnvVariablesCache.set(cacheKey, memCache); + sendTelemetryEvent( + EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, + stopWatch.elapsedTime, + { failed: false }, + ); + return vars; + }) + .catch((ex) => { + sendTelemetryEvent( + EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, + stopWatch.elapsedTime, + { failed: true }, + ); + throw ex; + }); + } + + @cache(-1, true) + public async getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise { + // Try to get the process environment variables using Python by printing variables, that can be little different + // from `process.env` and is preferred when calculating diff. + const globalInterpreters = this.interpreterService + .getInterpreters() + .filter((i) => !virtualEnvTypes.includes(i.envType)); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + try { + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgumentForPythonExt(); + }); + const command = `${interpreterPath} ${args.join(' ')}`; + const processService = await this.processServiceFactory.create(resource, { doNotUseCustomEnvs: true }); + const result = await processService.shellExec(command, { + shell, + timeout: ENVIRONMENT_TIMEOUT, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); + return returnedEnv ?? process.env; + } catch (ex) { + return process.env; + } + } + + public async getEnvironmentActivationShellCommands( + resource: Resource, + interpreter?: PythonEnvironment, + ): Promise { + const shellInfo = defaultShells[this.platform.osType]; + if (!shellInfo) { + return []; + } + return this.helper.getEnvironmentActivationShellCommands(resource, shellInfo.shellType, interpreter); } public async getActivatedEnvironmentVariablesImpl( resource: Resource, - interpreter?: PythonInterpreter, - allowExceptions?: boolean + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, ): Promise { - const shellInfo = defaultShells[this.platform.osType]; + let shellInfo = defaultShells[this.platform.osType]; if (!shellInfo) { - return; + return undefined; + } + if (shell) { + const customShellType = identifyShellFromShellPath(shell); + shellInfo = { shellType: customShellType, shell }; } - 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 customEnvVars = (await this.envVarsService.getEnvironmentVariables(resource)) ?? {}; const hasCustomEnvVars = Object.keys(customEnvVars).length; const env = hasCustomEnvVars ? customEnvVars : { ...this.currentProcess.env }; + let command: string | undefined; + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgumentForPythonExt(); + }); + if (interpreter?.envType === EnvironmentType.Conda) { + const conda = await Conda.getConda(shell); + const pythonArgv = await conda?.getRunPythonArgs({ + name: interpreter.envName, + prefix: interpreter.envPath ?? '', + }); + if (pythonArgv) { + // Using environment prefix isn't needed as the marker script already takes care of it. + command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); + } + } else if (interpreter?.envType === EnvironmentType.Pixi) { + const pythonArgv = await getRunPixiPythonCommand(interpreter.path); + if (pythonArgv) { + command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); + } + } + if (!command) { + const activationCommands = await this.helper.getEnvironmentActivationShellCommands( + resource, + shellInfo.shellType, + interpreter, + ); + traceVerbose( + `Activation Commands received ${activationCommands} for shell ${shellInfo.shell}, resource ${resource?.fsPath} and interpreter ${interpreter?.path}`, + ); + if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { + if (interpreter && [EnvironmentType.Venv, EnvironmentType.Pyenv].includes(interpreter?.envType)) { + const key = getSearchPathEnvVarNames()[0]; + if (env[key]) { + env[key] = `${path.dirname(interpreter.path)}${path.delimiter}${env[key]}`; + } else { + env[key] = `${path.dirname(interpreter.path)}`; + } + + return env; + } + return undefined; + } + const commandSeparator = [TerminalShellType.powershell, TerminalShellType.powershellCore].includes( + shellInfo.shellType, + ) + ? ';' + : '&&'; + // Run the activate command collect the environment from it. + const activationCommand = fixActivationCommands(activationCommands).join(` ${commandSeparator} `); + // In order to make sure we know where the environment output is, + // put in a dummy echo we can look for + command = `${activationCommand} ${commandSeparator} echo '${ENVIRONMENT_PREFIX}' ${commandSeparator} python ${args.join( + ' ', + )}`; + } + // 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: @@ -195,22 +311,48 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi // 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; + let returnedEnv: NodeJS.ProcessEnv | undefined; while (!result) { try { result = await processService.shellExec(command, { env, shell: shellInfo.shell, - timeout: getEnvironmentTimeout, + timeout: + interpreter?.envType === EnvironmentType.Conda + ? CONDA_ENVIRONMENT_TIMEOUT + : ENVIRONMENT_TIMEOUT, maxBuffer: 1000 * 1000, - throwOnStdErr: false + throwOnStdErr: false, }); - if (result.stderr && result.stderr.length > 0) { - throw new Error(`StdErr from ShellExec, ${result.stderr} for ${command}`); + + try { + // Try to parse the output, even if we have errors in stderr, its possible they are false positives. + // If variables are available, then ignore errors (but log them). + returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); + } catch (ex) { + if (!result.stderr) { + throw ex; + } + } + if (result.stderr) { + if (returnedEnv) { + traceWarn('Got env variables but with errors', result.stderr, returnedEnv); + if ( + result.stderr.includes('running scripts is disabled') || + result.stderr.includes('FullyQualifiedErrorId : UnauthorizedAccess') + ) { + throw new Error( + `Skipping returned result when powershell execution is disabled, stderr ${result.stderr} for ${command}`, + ); + } + } else { + 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(); + const excString = (exc as Error).toString(); if (condaRetryMessages.find((m) => excString.includes(m)) && tryCount < 10) { traceInfo(`Conda is busy, attempting to retry ...`); result = undefined; @@ -221,7 +363,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } } } - const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); // Put back the PYTHONWARNINGS value if (oldWarnings && returnedEnv) { @@ -233,8 +374,8 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } catch (e) { traceError('getActivatedEnvironmentVariables', e); sendTelemetryEvent(EventName.ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED, undefined, { - isPossiblyCondaEnv, - terminal: shellInfo.shellType + isPossiblyCondaEnv: interpreter?.envType === EnvironmentType.Conda, + terminal: shellInfo.shellType, }); // Some callers want this to bubble out, others don't @@ -242,17 +383,23 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi throw e; } } + return undefined; } - 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); + // eslint-disable-next-line class-methods-use-this + @traceDecoratorError('Failed to parse Environment variables') + @traceDecoratorVerbose('parseEnvironmentOutput', TraceOptions.None) + private parseEnvironmentOutput(output: string, parse: (out: string) => NodeJS.ProcessEnv | undefined) { + if (output.indexOf(ENVIRONMENT_PREFIX) === -1) { + return parse(output); + } + output = output.substring(output.indexOf(ENVIRONMENT_PREFIX) + ENVIRONMENT_PREFIX.length); const js = output.substring(output.indexOf('{')).trim(); return parse(js); } } + +function fixActivationCommands(commands: string[]): string[] { + // Replace 'source ' with '. ' as that works in shell exec + return commands.map((cmd) => cmd.replace(/^source\s+/, '. ')); +} diff --git a/src/client/interpreter/activation/terminalEnvironmentActivationService.ts b/src/client/interpreter/activation/terminalEnvironmentActivationService.ts deleted file mode 100644 index cf3a8363c9f1..000000000000 --- a/src/client/interpreter/activation/terminalEnvironmentActivationService.ts +++ /dev/null @@ -1,76 +0,0 @@ -// 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 { PythonInterpreter } 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?: PythonInterpreter | 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 index da9837d4071b..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -4,13 +4,20 @@ 'use strict'; import { Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { EnvironmentVariables } from '../../common/variables/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; export const IEnvironmentActivationService = Symbol('IEnvironmentActivationService'); export interface IEnvironmentActivationService { + getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise; getActivatedEnvironmentVariables( resource: Resource, - interpreter?: PythonInterpreter, - allowExceptions?: boolean + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, ): Promise; + getEnvironmentActivationShellCommands( + resource: Resource, + interpreter?: PythonEnvironment, + ): Promise; } diff --git a/src/client/interpreter/activation/wrapperEnvironmentActivationService.ts b/src/client/interpreter/activation/wrapperEnvironmentActivationService.ts deleted file mode 100644 index e9aa7a789b4e..000000000000 --- a/src/client/interpreter/activation/wrapperEnvironmentActivationService.ts +++ /dev/null @@ -1,214 +0,0 @@ -// 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 { PythonInterpreter } 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?: PythonInterpreter | 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?: PythonInterpreter, - 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?: PythonInterpreter, - 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?: PythonInterpreter | 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 deleted file mode 100644 index ca3b4d131e97..000000000000 --- a/src/client/interpreter/autoSelection/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 index 596d0a5962e5..5ad5362e8210 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -3,25 +3,22 @@ 'use strict'; -import { inject, injectable, named } from 'inversify'; -import { compare } from 'semver'; +import { inject, injectable } from 'inversify'; import { Event, EventEmitter, Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; +import { DiscoveryUsingWorkers } from '../../common/experiments/groups'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion'; +import { ProgressReportStage } from '../../pythonEnvironments/base/locator'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IInterpreterHelper } from '../contracts'; -import { - AutoSelectionRule, - IInterpreterAutoSelectionRule, - IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService, - IInterpreterSecurityService -} from './types'; +import { IInterpreterComparer } from '../configuration/types'; +import { IInterpreterHelper, IInterpreterService } from '../contracts'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './types'; const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; const workspacePathNameForGlobalWorkspaces = ''; @@ -29,100 +26,65 @@ 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[] = []; + + private readonly autoSelectedInterpreterByWorkspace = new Map(); + + private globallyPreferredInterpreter: IPersistentState< + PythonEnvironment | undefined + > = this.stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ); + 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(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IInterpreterComparer) private readonly envTypeComparer: IInterpreterComparer, + @inject(IInterpreterAutoSelectionProxyService) proxy: IInterpreterAutoSelectionProxyService, @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, - @inject(IInterpreterSecurityService) private readonly interpreterSecurityService: IInterpreterSecurityService + @inject(IExperimentService) private readonly experimentService: IExperimentService, ) { - // 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) + + /** + * Auto-select a Python environment from the list returned by environment discovery. + * If there's a cached auto-selected environment -> return it. + */ public async autoSelectInterpreter(resource: Resource): Promise { const key = this.getWorkspacePathKey(resource); - if (!this.autoSelectedWorkspacePromises.has(key)) { + const useCachedInterpreter = this.autoSelectedWorkspacePromises.has(key); + + if (!useCachedInterpreter) { 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(); + await this.autoselectInterpreterWithLocators(resource); + deferred.resolve(); } + + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, undefined, { + useCachedInterpreter, + }); + return this.autoSelectedWorkspacePromises.get(key)!.promise; } + public get onDidChangeAutoSelectedInterpreter(): Event { return this.didAutoSelectedInterpreterEmitter.event; } - public getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined { + + 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): PythonInterpreter | undefined { const workspaceState = this.getWorkspaceState(resource); if (workspaceState && workspaceState.value) { return workspaceState.value; @@ -135,20 +97,26 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.globallyPreferredInterpreter.value; } - public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined) { + + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise { await this.storeAutoSelectedInterpreter(resource, interpreter); } - public async setGlobalInterpreter(interpreter: PythonInterpreter) { + + public async setGlobalInterpreter(interpreter: PythonEnvironment): Promise { await this.storeAutoSelectedInterpreter(undefined, interpreter); } - protected async clearWorkspaceStoreIfInvalid(resource: Resource) { + + protected async clearWorkspaceStoreIfInvalid(resource: Resource): Promise { 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: PythonInterpreter | undefined) { + + protected async storeAutoSelectedInterpreter( + resource: Resource, + interpreter: PythonEnvironment | undefined, + ): Promise { const workspaceFolderPath = this.getWorkspacePathKey(resource); if (workspaceFolderPath === workspacePathNameForGlobalWorkspaces) { // Update store only if this version is better. @@ -157,7 +125,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio this.globallyPreferredInterpreter.value.version && interpreter && interpreter.version && - compare(this.globallyPreferredInterpreter.value.version.raw, interpreter.version.raw) > 0 + compareSemVerLikeVersions(this.globallyPreferredInterpreter.value.version, interpreter.version) > 0 ) { return; } @@ -173,7 +141,8 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); } } - protected async initializeStore(resource: Resource) { + + protected async initializeStore(resource: Resource): Promise { const workspaceFolderPath = this.getWorkspacePathKey(resource); // Since we're initializing for this resource, // Ensure any cached information for this workspace have been removed. @@ -183,9 +152,10 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } await this.clearStoreIfFileIsInvalid(); } + private async clearStoreIfFileIsInvalid() { this.globallyPreferredInterpreter = this.stateFactory.createGlobalPersistentState< - PythonInterpreter | undefined + PythonEnvironment | undefined >(preferredGlobalInterpreter, undefined); if ( this.globallyPreferredInterpreter.value && @@ -194,15 +164,95 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio await this.globallyPreferredInterpreter.updateValue(undefined); } } + private getWorkspacePathKey(resource: Resource): string { return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); } - private getWorkspaceState(resource: Resource): undefined | IPersistentState { + + private getWorkspaceState(resource: Resource): undefined | IPersistentState { const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); - if (!workspaceUri) { - return; + if (workspaceUri) { + const key = `autoSelectedWorkspacePythonInterpreter-${workspaceUri.folderUri.fsPath}`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); } - const key = `autoSelectedWorkspacePythonInterpreter-${workspaceUri.folderUri.fsPath}`; + return undefined; + } + + private getAutoSelectionInterpretersQueryState(resource: Resource): IPersistentState { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const key = `autoSelectionInterpretersQueried-${workspaceUri?.folderUri.fsPath || 'global'}`; return this.stateFactory.createWorkspacePersistentState(key, undefined); } + + private getAutoSelectionQueriedOnceState(): IPersistentState { + const key = `autoSelectionInterpretersQueriedOnce`; + return this.stateFactory.createGlobalPersistentState(key, undefined); + } + + /** + * Auto-selection logic: + * 1. If there are cached interpreters (not the first session in this workspace) + * -> sort using the same logic as in the interpreter quickpick and return the first one; + * 2. If not, we already fire all the locators, so wait for their response, sort the interpreters and return the first one. + * + * `getInterpreters` will check the cache first and return early if there are any cached interpreters, + * and if not it will wait for locators to return. + * As such, we can sort interpreters based on what it returns. + */ + private async autoselectInterpreterWithLocators(resource: Resource): Promise { + // Do not perform a full interpreter search if we already have cached interpreters for this workspace. + const queriedState = this.getAutoSelectionInterpretersQueryState(resource); + const globalQueriedState = this.getAutoSelectionQueriedOnceState(); + if (globalQueriedState.value && queriedState.value !== true && resource) { + await this.interpreterService.triggerRefresh({ + searchLocations: { roots: [resource], doNotIncludeNonRooted: true }, + }); + } + + await this.envTypeComparer.initialize(resource); + const inExperiment = this.experimentService.inExperimentSync(DiscoveryUsingWorkers.experiment); + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + let recommendedInterpreter: PythonEnvironment | undefined; + if (inExperiment) { + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + // Do not wait for validation of all interpreters to finish, we only need to validate the recommended interpreter. + await this.interpreterService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + } + let interpreters = this.interpreterService.getInterpreters(resource); + + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + const details = recommendedInterpreter + ? await this.interpreterService.getInterpreterDetails(recommendedInterpreter.path) + : undefined; + if (!details || !recommendedInterpreter) { + await this.interpreterService.refreshPromise; // Interpreter is invalid, wait for all of validation to finish. + interpreters = this.interpreterService.getInterpreters(resource); + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + } + } else { + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + await this.interpreterService.refreshPromise; + } + const interpreters = this.interpreterService.getInterpreters(resource); + + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + } + if (!recommendedInterpreter) { + return; + } + if (workspaceUri) { + this.setWorkspaceInterpreter(workspaceUri.folderUri, recommendedInterpreter); + } else { + this.setGlobalInterpreter(recommendedInterpreter); + } + + queriedState.updateValue(true); + globalQueriedState.updateValue(true); + + this.didAutoSelectedInterpreterEmitter.fire(); + } } diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts deleted file mode 100644 index 359312ac6ca6..000000000000 --- a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts +++ /dev/null @@ -1,93 +0,0 @@ -// 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 { PythonInterpreter } 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: PythonInterpreter, 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: PythonInterpreter, 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 deleted file mode 100644 index 827390b40745..000000000000 --- a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts +++ /dev/null @@ -1,53 +0,0 @@ -// 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 { PythonInterpreter } 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: PythonInterpreter, 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: PythonInterpreter, 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 deleted file mode 100644 index f75317a859cb..000000000000 --- a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts +++ /dev/null @@ -1,99 +0,0 @@ -// 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 index 02806dccffbe..ea9be593d386 100644 --- a/src/client/interpreter/autoSelection/proxy.ts +++ b/src/client/interpreter/autoSelection/proxy.ts @@ -5,28 +5,34 @@ import { inject, injectable } from 'inversify'; import { Event, EventEmitter, Uri } from 'vscode'; -import { IAsyncDisposableRegistry, IDisposableRegistry, Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { IInterpreterAutoSeletionProxyService } from './types'; +import { IDisposableRegistry, Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IInterpreterAutoSelectionProxyService } from './types'; @injectable() -export class InterpreterAutoSeletionProxyService implements IInterpreterAutoSeletionProxyService { +export class InterpreterAutoSelectionProxyService implements IInterpreterAutoSelectionProxyService { private readonly didAutoSelectedInterpreterEmitter = new EventEmitter(); - private instance?: IInterpreterAutoSeletionProxyService; - constructor(@inject(IDisposableRegistry) private readonly disposables: IAsyncDisposableRegistry) {} - public registerInstance(instance: IInterpreterAutoSeletionProxyService): void { + + private instance?: IInterpreterAutoSelectionProxyService; + + constructor(@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry) {} + + public registerInstance(instance: IInterpreterAutoSelectionProxyService): void { this.instance = instance; this.disposables.push( - this.instance.onDidChangeAutoSelectedInterpreter(() => this.didAutoSelectedInterpreterEmitter.fire()) + this.instance.onDidChangeAutoSelectedInterpreter(() => this.didAutoSelectedInterpreterEmitter.fire()), ); } + public get onDidChangeAutoSelectedInterpreter(): Event { return this.didAutoSelectedInterpreterEmitter.event; } - public getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined { + + public getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined { return this.instance ? this.instance.getAutoSelectedInterpreter(resource) : undefined; } - public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined): Promise { + + 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 deleted file mode 100644 index 5a6c56abb08e..000000000000 --- a/src/client/interpreter/autoSelection/rules/baseRule.ts +++ /dev/null @@ -1,111 +0,0 @@ -// 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 { PythonInterpreter } 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): PythonInterpreter | 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?: PythonInterpreter, - 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: PythonInterpreter | 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 deleted file mode 100644 index 134a53203051..000000000000 --- a/src/client/interpreter/autoSelection/rules/cached.ts +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index a45f9cbbc98b..000000000000 --- a/src/client/interpreter/autoSelection/rules/currentPath.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index eae85e915ca2..000000000000 --- a/src/client/interpreter/autoSelection/rules/settings.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index 1139396cea68..000000000000 --- a/src/client/interpreter/autoSelection/rules/system.ts +++ /dev/null @@ -1,45 +0,0 @@ -// 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 { InterpreterType } 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.type !== InterpreterType.VirtualEnv && - int.type !== InterpreterType.Venv && - int.type !== InterpreterType.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 deleted file mode 100644 index 2442e52eaf24..000000000000 --- a/src/client/interpreter/autoSelection/rules/winRegistry.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index 3265bf94a142..000000000000 --- a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts +++ /dev/null @@ -1,99 +0,0 @@ -// 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 { PythonInterpreter } 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 index d0fcbf9f9f7f..91d0224717d4 100644 --- a/src/client/interpreter/autoSelection/types.ts +++ b/src/client/interpreter/autoSelection/types.ts @@ -4,34 +4,30 @@ 'use strict'; import { Event, Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { IPersistentState, Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; -export const IInterpreterAutoSeletionProxyService = Symbol('IInterpreterAutoSeletionProxyService'); +export const IInterpreterAutoSelectionProxyService = Symbol('IInterpreterAutoSelectionProxyService'); /** * 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 { +export interface IInterpreterAutoSelectionProxyService { readonly onDidChangeAutoSelectedInterpreter: Event; - getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; - registerInstance?(instance: IInterpreterAutoSeletionProxyService): void; - setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined): Promise; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + registerInstance?(instance: IInterpreterAutoSelectionProxyService): void; + setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise; } export const IInterpreterAutoSelectionService = Symbol('IInterpreterAutoSelectionService'); -export interface IInterpreterAutoSelectionService extends IInterpreterAutoSeletionProxyService { +export interface IInterpreterAutoSelectionService extends IInterpreterAutoSelectionProxyService { readonly onDidChangeAutoSelectedInterpreter: Event; autoSelectInterpreter(resource: Resource): Promise; - getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; - setGlobalInterpreter(interpreter: PythonInterpreter | undefined): Promise; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + setGlobalInterpreter(interpreter: PythonEnvironment | undefined): Promise; } export enum AutoSelectionRule { @@ -41,34 +37,5 @@ export enum AutoSelectionRule { 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): PythonInterpreter | undefined; -} - -export const IInterpreterSecurityService = Symbol('IInterpreterSecurityService'); -export interface IInterpreterSecurityService { - readonly onDidChangeSafeInterpreters: Event; - evaluateAndRecordInterpreterSafety(interpreter: PythonInterpreter, resource: Resource): Promise; - isSafe(interpreter: PythonInterpreter, 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: PythonInterpreter, resource: Resource): Promise; - inferValueUsingCurrentState(interpreter: PythonInterpreter, resource: Resource): boolean | undefined; + windowsRegistry = 'windowsRegistry', } diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts new file mode 100644 index 000000000000..2e1013b7b5a8 --- /dev/null +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable, inject } from 'inversify'; +import { Resource } from '../../common/types'; +import { Architecture } from '../../common/utils/platform'; +import { isActiveStateEnvironmentForWorkspace } from '../../pythonEnvironments/common/environmentManagers/activestate'; +import { isParentPath } from '../../pythonEnvironments/common/externalDependencies'; +import { + EnvironmentType, + PythonEnvironment, + virtualEnvTypes, + workspaceVirtualEnvTypes, +} from '../../pythonEnvironments/info'; +import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion'; +import { IInterpreterHelper } from '../contracts'; +import { IInterpreterComparer } from './types'; +import { getActivePyenvForDirectory } from '../../pythonEnvironments/common/environmentManagers/pyenv'; +import { arePathsSame } from '../../common/platform/fs-paths'; + +export enum EnvLocationHeuristic { + /** + * Environments inside the workspace. + */ + Local = 1, + /** + * Environments outside the workspace. + */ + Global = 2, +} + +@injectable() +export class EnvironmentTypeComparer implements IInterpreterComparer { + private workspaceFolderPath: string; + + private preferredPyenvInterpreterPath = new Map(); + + constructor(@inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper) { + this.workspaceFolderPath = this.interpreterHelper.getActiveWorkspaceUri(undefined)?.folderUri.fsPath ?? ''; + } + + /** + * Compare 2 Python environments, sorting them by assumed usefulness. + * Return 0 if both environments are equal, -1 if a should be closer to the beginning of the list, or 1 if a comes after b. + * + * The comparison guidelines are: + * 1. Local environments first (same path as the workspace root); + * 2. Global environments next (anything not local), with conda environments at a lower priority, and "base" being last; + * 3. Globally-installed interpreters (/usr/bin/python3, Microsoft Store). + * + * Always sort with newest version of Python first within each subgroup. + */ + public compare(a: PythonEnvironment, b: PythonEnvironment): number { + if (isProblematicCondaEnvironment(a)) { + return 1; + } + if (isProblematicCondaEnvironment(b)) { + return -1; + } + // Check environment location. + const envLocationComparison = compareEnvironmentLocation(a, b, this.workspaceFolderPath); + if (envLocationComparison !== 0) { + return envLocationComparison; + } + + if (a.envType === EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) { + const preferredPyenv = this.preferredPyenvInterpreterPath.get(this.workspaceFolderPath); + if (preferredPyenv) { + if (arePathsSame(preferredPyenv, b.path)) { + return 1; + } + if (arePathsSame(preferredPyenv, a.path)) { + return -1; + } + } + } + + // Check environment type. + const envTypeComparison = compareEnvironmentType(a, b); + if (envTypeComparison !== 0) { + return envTypeComparison; + } + + // Check Python version. + const versionComparison = comparePythonVersionDescending(a.version, b.version); + if (versionComparison !== 0) { + return versionComparison; + } + + // If we have the "base" Conda env, put it last in its Python version subgroup. + if (isBaseCondaEnvironment(a)) { + return 1; + } + + if (isBaseCondaEnvironment(b)) { + return -1; + } + + // Check alphabetical order. + const nameA = getSortName(a, this.interpreterHelper); + const nameB = getSortName(b, this.interpreterHelper); + if (nameA === nameB) { + return 0; + } + + return nameA > nameB ? 1 : -1; + } + + public async initialize(resource: Resource): Promise { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const cwd = workspaceUri?.folderUri.fsPath; + if (!cwd) { + return; + } + const preferredPyenvInterpreter = await getActivePyenvForDirectory(cwd); + this.preferredPyenvInterpreterPath.set(cwd, preferredPyenvInterpreter); + } + + public getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined { + // When recommending an intepreter for a workspace, we either want to return a local one + // or fallback on a globally-installed interpreter, and we don't want want to suggest a global environment + // because we would have to add a way to match environments to a workspace. + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const filteredInterpreters = interpreters.filter((i) => { + if (isProblematicCondaEnvironment(i)) { + return false; + } + if ( + i.envType === EnvironmentType.ActiveState && + (!i.path || + !workspaceUri || + !isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) + ) { + return false; + } + if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) { + return true; + } + if (!workspaceVirtualEnvTypes.includes(i.envType) && virtualEnvTypes.includes(i.envType)) { + // These are global virtual envs so we're not sure if these envs were created for the workspace, skip them. + return false; + } + if (i.version?.major === 2) { + return false; + } + return true; + }); + filteredInterpreters.sort(this.compare.bind(this)); + return filteredInterpreters.length ? filteredInterpreters[0] : undefined; + } +} + +function getSortName(info: PythonEnvironment, interpreterHelper: IInterpreterHelper): 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(getArchitectureSortName(info.architecture)); + } + if (info.companyDisplayName && info.companyDisplayName.length > 0) { + sortNameParts.push(info.companyDisplayName.trim()); + } else { + sortNameParts.push('Python'); + } + + if (info.envType) { + const name = 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(); +} + +function getArchitectureSortName(arch?: Architecture) { + // Strings are choosen keeping in mind that 64-bit gets preferred over 32-bit. + switch (arch) { + case Architecture.x64: + return 'x64'; + case Architecture.x86: + return 'x86'; + default: + return ''; + } +} + +function isBaseCondaEnvironment(environment: PythonEnvironment): boolean { + return ( + environment.envType === EnvironmentType.Conda && + (environment.envName === 'base' || environment.envName === 'miniconda') + ); +} + +export function isProblematicCondaEnvironment(environment: PythonEnvironment): boolean { + return environment.envType === EnvironmentType.Conda && environment.path === 'python'; +} + +/** + * Compare 2 Python versions in decending order, most recent one comes first. + */ +function comparePythonVersionDescending(a: PythonVersion | undefined, b: PythonVersion | undefined): number { + if (!a) { + return 1; + } + + if (!b) { + return -1; + } + + if (a.raw === b.raw) { + return 0; + } + + if (a.major === b.major) { + if (a.minor === b.minor) { + if (a.patch === b.patch) { + return a.build.join(' ') > b.build.join(' ') ? -1 : 1; + } + return a.patch > b.patch ? -1 : 1; + } + return a.minor > b.minor ? -1 : 1; + } + + return a.major > b.major ? -1 : 1; +} + +/** + * Compare 2 environment locations: return 0 if they are the same, -1 if a comes before b, 1 otherwise. + */ +function compareEnvironmentLocation(a: PythonEnvironment, b: PythonEnvironment, workspacePath: string): number { + const aHeuristic = getEnvLocationHeuristic(a, workspacePath); + const bHeuristic = getEnvLocationHeuristic(b, workspacePath); + + return Math.sign(aHeuristic - bHeuristic); +} + +/** + * Return a heuristic value depending on the environment type. + */ +export function getEnvLocationHeuristic(environment: PythonEnvironment, workspacePath: string): EnvLocationHeuristic { + if ( + workspacePath.length > 0 && + ((environment.envPath && isParentPath(environment.envPath, workspacePath)) || + (environment.path && isParentPath(environment.path, workspacePath))) + ) { + return EnvLocationHeuristic.Local; + } + return EnvLocationHeuristic.Global; +} + +/** + * Compare 2 environment types: return 0 if they are the same, -1 if a comes before b, 1 otherwise. + */ +function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment): number { + if (!a.type && !b.type) { + // Unless one of them is pyenv interpreter, return 0 if two global interpreters are being compared. + if (a.envType === EnvironmentType.Pyenv && b.envType !== EnvironmentType.Pyenv) { + return -1; + } + if (a.envType !== EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) { + return 1; + } + return 0; + } + const envTypeByPriority = getPrioritizedEnvironmentType(); + return Math.sign(envTypeByPriority.indexOf(a.envType) - envTypeByPriority.indexOf(b.envType)); +} + +function getPrioritizedEnvironmentType(): EnvironmentType[] { + return [ + // Prioritize non-Conda environments. + EnvironmentType.Poetry, + EnvironmentType.Pipenv, + EnvironmentType.VirtualEnvWrapper, + EnvironmentType.Hatch, + EnvironmentType.Venv, + EnvironmentType.VirtualEnv, + EnvironmentType.ActiveState, + EnvironmentType.Conda, + EnvironmentType.Pyenv, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.System, + EnvironmentType.Unknown, + ]; +} diff --git a/src/client/interpreter/configuration/interpreterComparer.ts b/src/client/interpreter/configuration/interpreterComparer.ts deleted file mode 100644 index 0182ce2e5ee6..000000000000 --- a/src/client/interpreter/configuration/interpreterComparer.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 { PythonInterpreter } 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: PythonInterpreter, b: PythonInterpreter): number { - const nameA = this.getSortName(a); - const nameB = this.getSortName(b); - if (nameA === nameB) { - return 0; - } - return nameA > nameB ? 1 : -1; - } - private getSortName(info: PythonInterpreter): 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.type) { - const name = this.interpreterHelper.getInterpreterTypeDisplayName(info.type); - 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 index d47bcab9aff5..6307e286dbfe 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts @@ -8,18 +8,23 @@ 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 { IConfigurationService, IDisposable, IPathUtils, Resource } from '../../../../common/types'; +import { Common, Interpreters } from '../../../../common/utils/localize'; import { IPythonPathUpdaterServiceManager } from '../../types'; - +export interface WorkspaceSelectionQuickPickItem extends QuickPickItem { + uri?: Uri; +} @injectable() export abstract class BaseInterpreterSelectorCommand implements IExtensionSingleActivationService, IDisposable { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; protected disposables: Disposable[] = []; constructor( @unmanaged() protected readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, @unmanaged() protected readonly commandManager: ICommandManager, @unmanaged() protected readonly applicationShell: IApplicationShell, - @unmanaged() protected readonly workspaceService: IWorkspaceService + @unmanaged() protected readonly workspaceService: IWorkspaceService, + @unmanaged() protected readonly pathUtils: IPathUtils, + @unmanaged() protected readonly configurationService: IConfigurationService, ) { this.disposables.push(this); } @@ -28,54 +33,87 @@ export abstract class BaseInterpreterSelectorCommand implements IExtensionSingle this.disposables.forEach((disposable) => disposable.dispose()); } - public abstract async activate(): Promise; + public abstract activate(): Promise; - protected async getConfigTarget(): Promise< + protected async getConfigTargets(options?: { + resetTarget?: boolean; + }): Promise< | { folderUri: Resource; configTarget: ConfigurationTarget; - } + }[] | undefined > { - if ( - !Array.isArray(this.workspaceService.workspaceFolders) || - this.workspaceService.workspaceFolders.length === 0 - ) { - return { - folderUri: undefined, - configTarget: ConfigurationTarget.Global - }; + const workspaceFolders = this.workspaceService.workspaceFolders; + if (workspaceFolders === undefined || 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 - }; + if (workspaceFolders.length === 1) { + return [ + { + folderUri: 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 - })), + let quickPickItems: WorkspaceSelectionQuickPickItem[] = options?.resetTarget + ? [ + { + label: Common.clearAll, + }, + ] + : []; + quickPickItems.push( + ...workspaceFolders.map((w) => { + const selectedInterpreter = this.pathUtils.getDisplayName( + this.configurationService.getSettings(w.uri).pythonPath, + w.uri.fsPath, + ); + return { + label: w.name, + description: this.pathUtils.getDisplayName(path.dirname(w.uri.fsPath)), + uri: w.uri, + detail: selectedInterpreter, + }; + }), { - label: Interpreters.entireWorkspace(), - uri: this.workspaceService.workspaceFolders[0].uri - } - ]; + label: options?.resetTarget ? Interpreters.clearAtWorkspace : Interpreters.entireWorkspace, + uri: workspaceFolders[0].uri, + }, + ); const selection = await this.applicationShell.showQuickPick(quickPickItems, { - placeHolder: 'Select the workspace to set the interpreter' + placeHolder: options?.resetTarget + ? 'Select the workspace folder to clear the interpreter for' + : 'Select the workspace folder to set the interpreter', }); + if (selection?.label === Common.clearAll) { + const folderTargets: { + folderUri: Resource; + configTarget: ConfigurationTarget; + }[] = workspaceFolders.map((w) => ({ + folderUri: w.uri, + configTarget: ConfigurationTarget.WorkspaceFolder, + })); + return [ + ...folderTargets, + { folderUri: workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }, + ]; + } + return selection - ? selection.label === Interpreters.entireWorkspace() - ? { folderUri: selection.uri, configTarget: ConfigurationTarget.Workspace } - : { folderUri: selection.uri, configTarget: ConfigurationTarget.WorkspaceFolder } + ? selection.label === Interpreters.entireWorkspace || selection.label === Interpreters.clearAtWorkspace + ? [{ folderUri: selection.uri, configTarget: ConfigurationTarget.Workspace }] + : [{ folderUri: selection.uri, configTarget: ConfigurationTarget.WorkspaceFolder }] : undefined; } } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts new file mode 100644 index 000000000000..d6d423c1eab8 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.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 { IExtensionSingleActivationService } from '../../../../../activation/types'; +import { ExtensionContextKey } from '../../../../../common/application/contextKeys'; +import { ICommandManager, IContextKeyManager } from '../../../../../common/application/types'; +import { PythonWelcome } from '../../../../../common/application/walkThroughs'; +import { Commands, PVSC_EXTENSION_ID } from '../../../../../common/constants'; +import { IBrowserService, IDisposableRegistry } from '../../../../../common/types'; +import { IPlatformService } from '../../../../../common/platform/types'; + +@injectable() +export class InstallPythonCommand implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, + @inject(IBrowserService) private readonly browserService: IBrowserService, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallPython, () => this._installPython())); + } + + public async _installPython(): Promise { + if (this.platformService.isWindows) { + const version = await this.platformService.getVersion(); + if (version.major > 8) { + // OS is not Windows 8, ms-windows-store URIs are available: + // https://docs.microsoft.com/en-us/windows/uwp/launch-resume/launch-store-app + this.browserService.launch('ms-windows-store://pdp/?ProductId=9NRWMJP3717K'); + return; + } + } + this.showInstallPythonTile(); + } + + private showInstallPythonTile() { + this.contextManager.setContext(ExtensionContextKey.showInstallPythonTile, true); + let step: string; + if (this.platformService.isWindows) { + step = PythonWelcome.windowsInstallId; + } else if (this.platformService.isLinux) { + step = PythonWelcome.linuxInstallId; + } else { + step = PythonWelcome.macOSInstallId; + } + this.commandManager.executeCommand( + 'workbench.action.openWalkthrough', + { + category: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}`, + step: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}#${step}`, + }, + false, + ); + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts new file mode 100644 index 000000000000..3b4a6d428baa --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts @@ -0,0 +1,115 @@ +/* eslint-disable global-require */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type * as whichTypes from 'which'; +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../../../activation/types'; +import { Commands } from '../../../../../common/constants'; +import { IDisposableRegistry } from '../../../../../common/types'; +import { ICommandManager, ITerminalManager } from '../../../../../common/application/types'; +import { sleep } from '../../../../../common/utils/async'; +import { OSType } from '../../../../../common/utils/platform'; +import { traceVerbose } from '../../../../../logging'; +import { Interpreters } from '../../../../../common/utils/localize'; + +enum PackageManagers { + brew = 'brew', + apt = 'apt', + dnf = 'dnf', +} + +/** + * Runs commands listed in walkthrough to install Python. + */ +@injectable() +export class InstallPythonViaTerminal implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false }; + + private readonly packageManagerCommands: Record = { + brew: ['brew install python3'], + dnf: ['sudo dnf install python3'], + apt: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'], + }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand(Commands.InstallPythonOnMac, () => + this._installPythonOnUnix(OSType.OSX), + ), + ); + this.disposables.push( + this.commandManager.registerCommand(Commands.InstallPythonOnLinux, () => + this._installPythonOnUnix(OSType.Linux), + ), + ); + } + + public async _installPythonOnUnix(os: OSType.Linux | OSType.OSX): Promise { + const commands = await this.getCommands(os); + const installMessage = + os === OSType.OSX + ? Interpreters.installPythonTerminalMacMessage + : Interpreters.installPythonTerminalMessageLinux; + const terminal = this.terminalManager.createTerminal({ + name: 'Python', + message: commands.length ? undefined : installMessage, + }); + terminal.show(true); + await waitForTerminalToStartup(); + for (const command of commands) { + terminal.sendText(command); + await waitForCommandToProcess(); + } + } + + private async getCommands(os: OSType.Linux | OSType.OSX) { + if (os === OSType.OSX) { + return this.getCommandsForPackageManagers([PackageManagers.brew]); + } + if (os === OSType.Linux) { + return this.getCommandsForPackageManagers([PackageManagers.apt, PackageManagers.dnf]); + } + throw new Error('OS not supported'); + } + + private async getCommandsForPackageManagers(packageManagers: PackageManagers[]) { + for (const packageManager of packageManagers) { + if (await isPackageAvailable(packageManager)) { + return this.packageManagerCommands[packageManager]; + } + } + return []; + } +} + +async function isPackageAvailable(packageManager: PackageManagers) { + try { + const which = require('which') as typeof whichTypes; + const resolvedPath = await which.default(packageManager); + traceVerbose(`Resolved path to ${packageManager} module:`, resolvedPath); + return resolvedPath.trim().length > 0; + } catch (ex) { + traceVerbose(`${packageManager} not found`, ex); + return false; + } +} + +async function waitForTerminalToStartup() { + // Sometimes the terminal takes some time to start up before it can start accepting input. + await sleep(100); +} + +async function waitForCommandToProcess() { + // 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/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts index b605b15389be..c10f90781adb 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts @@ -6,8 +6,11 @@ import { inject, injectable } from 'inversify'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../common/application/types'; import { Commands } from '../../../../common/constants'; +import { IConfigurationService, IPathUtils } from '../../../../common/types'; import { IPythonPathUpdaterServiceManager } from '../../types'; import { BaseInterpreterSelectorCommand } from './base'; +import { useEnvExtension } from '../../../../envExt/api.internal'; +import { resetInterpreterLegacy } from '../../../../envExt/api.legacy'; @injectable() export class ResetInterpreterCommand extends BaseInterpreterSelectorCommand { @@ -15,25 +18,40 @@ export class ResetInterpreterCommand extends BaseInterpreterSelectorCommand { @inject(IPythonPathUpdaterServiceManager) pythonPathUpdaterService: IPythonPathUpdaterServiceManager, @inject(ICommandManager) commandManager: ICommandManager, @inject(IApplicationShell) applicationShell: IApplicationShell, - @inject(IWorkspaceService) workspaceService: IWorkspaceService + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IPathUtils) pathUtils: IPathUtils, + @inject(IConfigurationService) configurationService: IConfigurationService, ) { - super(pythonPathUpdaterService, commandManager, applicationShell, workspaceService); + super( + pythonPathUpdaterService, + commandManager, + applicationShell, + workspaceService, + pathUtils, + configurationService, + ); } public async activate() { this.disposables.push( - this.commandManager.registerCommand(Commands.ClearWorkspaceInterpreter, this.resetInterpreter.bind(this)) + this.commandManager.registerCommand(Commands.ClearWorkspaceInterpreter, this.resetInterpreter.bind(this)), ); } public async resetInterpreter() { - const targetConfig = await this.getConfigTarget(); - if (!targetConfig) { + const targetConfigs = await this.getConfigTargets({ resetTarget: true }); + if (!targetConfigs) { return; } - const configTarget = targetConfig.configTarget; - const wkspace = targetConfig.folderUri; - - await this.pythonPathUpdaterService.updatePythonPath(undefined, configTarget, 'ui', wkspace); + await Promise.all( + targetConfigs.map(async (targetConfig) => { + const configTarget = targetConfig.configTarget; + const wkspace = targetConfig.folderUri; + await this.pythonPathUpdaterService.updatePythonPath(undefined, configTarget, 'ui', wkspace); + if (useEnvExtension()) { + await resetInterpreterLegacy(wkspace); + } + }), + ); } } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 3c021195ef31..a629d1bc793c 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -4,139 +4,720 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { QuickPickItem } from 'vscode'; +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import { + l10n, + QuickInputButton, + QuickInputButtons, + QuickPick, + QuickPickItem, + QuickPickItemKind, + ThemeIcon, +} from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../common/application/types'; -import { Commands } from '../../../../common/constants'; +import { Commands, Octicons, ThemeIcons } from '../../../../common/constants'; +import { isParentPath } from '../../../../common/platform/fs-paths'; import { IPlatformService } from '../../../../common/platform/types'; import { IConfigurationService, IPathUtils, Resource } from '../../../../common/types'; -import { InterpreterQuickPickList } from '../../../../common/utils/localize'; +import { Common, InterpreterQuickPickList } from '../../../../common/utils/localize'; +import { noop } from '../../../../common/utils/misc'; import { IMultiStepInput, IMultiStepInputFactory, + InputFlowAction, InputStep, - IQuickPickParameters + IQuickPickParameters, + QuickInputButtonSetup, } from '../../../../common/utils/multiStepInput'; +import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { TriggerRefreshOptions } from '../../../../pythonEnvironments/base/locator'; +import { EnvironmentType, PythonEnvironment } from '../../../../pythonEnvironments/info'; import { captureTelemetry, sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; -import { IInterpreterQuickPickItem, IInterpreterSelector, IPythonPathUpdaterServiceManager } from '../../types'; +import { IInterpreterService, PythonEnvironmentsChangedEvent } from '../../../contracts'; +import { isProblematicCondaEnvironment } from '../../environmentTypeComparer'; +import { + IInterpreterQuickPick, + IInterpreterQuickPickItem, + IInterpreterSelector, + InterpreterQuickPickParams, + IPythonPathUpdaterServiceManager, + ISpecialQuickPickItem, +} from '../../types'; import { BaseInterpreterSelectorCommand } from './base'; +import { untildify } from '../../../../common/helpers'; +import { useEnvExtension } from '../../../../envExt/api.internal'; +import { setInterpreterLegacy } from '../../../../envExt/api.legacy'; +import { CreateEnvironmentResult } from '../../../../pythonEnvironments/creation/proposed.createEnvApis'; export type InterpreterStateArgs = { path?: string; workspace: Resource }; +export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; + +function isInterpreterQuickPickItem(item: QuickPickType): item is IInterpreterQuickPickItem { + return 'interpreter' in item; +} + +function isSpecialQuickPickItem(item: QuickPickType): item is ISpecialQuickPickItem { + return 'alwaysShow' in item; +} + +function isSeparatorItem(item: QuickPickType): item is QuickPickItem { + return 'kind' in item && item.kind === QuickPickItemKind.Separator; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace EnvGroups { + export const Workspace = InterpreterQuickPickList.workspaceGroupName; + export const Conda = 'Conda'; + export const Global = InterpreterQuickPickList.globalGroupName; + export const VirtualEnv = 'VirtualEnv'; + export const PipEnv = 'PipEnv'; + export const Pyenv = 'Pyenv'; + export const Venv = 'Venv'; + export const Poetry = 'Poetry'; + export const Hatch = 'Hatch'; + export const Pixi = 'Pixi'; + export const VirtualEnvWrapper = 'VirtualEnvWrapper'; + export const ActiveState = 'ActiveState'; + export const Recommended = Common.recommended; +} + @injectable() -export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { +export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implements IInterpreterQuickPick { + private readonly createEnvironmentSuggestion: QuickPickItem = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, + alwaysShow: true, + }; + + private readonly manualEntrySuggestion: ISpecialQuickPickItem = { + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, + alwaysShow: true, + }; + + private readonly refreshButton = { + iconPath: new ThemeIcon(ThemeIcons.Refresh), + tooltip: InterpreterQuickPickList.refreshInterpreterList, + }; + + private readonly noPythonInstalled: ISpecialQuickPickItem = { + label: `${Octicons.Error} ${InterpreterQuickPickList.noPythonInstalled}`, + detail: InterpreterQuickPickList.clickForInstructions, + alwaysShow: true, + }; + + private wasNoPythonInstalledItemClicked = false; + + private readonly tipToReloadWindow: ISpecialQuickPickItem = { + label: `${Octicons.Lightbulb} Reload the window if you installed Python but don't see it`, + detail: `Click to run \`Developer: Reload Window\` command`, + alwaysShow: true, + }; + constructor( @inject(IApplicationShell) applicationShell: IApplicationShell, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IPathUtils) pathUtils: IPathUtils, @inject(IPythonPathUpdaterServiceManager) pythonPathUpdaterService: IPythonPathUpdaterServiceManager, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IConfigurationService) 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 + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, ) { - super(pythonPathUpdaterService, commandManager, applicationShell, workspaceService); + super( + pythonPathUpdaterService, + commandManager, + applicationShell, + workspaceService, + pathUtils, + configurationService, + ); } - public async activate() { + public async activate(): Promise { this.disposables.push( - this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)) + this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)), ); } public async _pickInterpreter( input: IMultiStepInput, - state: InterpreterStateArgs + state: InterpreterStateArgs, + filter?: (i: PythonEnvironment) => boolean, + params?: InterpreterQuickPickParams, ): 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( + // If the list is refreshing, it's crucial to maintain sorting order at all + // times so that the visible items do not change. + const preserveOrderWhenFiltering = !!this.interpreterService.refreshPromise; + const suggestions = this._getItems(state.workspace, filter, params); + state.path = undefined; + const currentInterpreterPathDisplay = this.pathUtils.getDisplayName( this.configurationService.getSettings(state.workspace).pythonPath, - state.workspace ? state.workspace.fsPath : undefined + state.workspace ? state.workspace.fsPath : undefined, ); + const placeholder = + params?.placeholder === null + ? undefined + : params?.placeholder ?? l10n.t('Selected Interpreter: {0}', currentInterpreterPathDisplay); + const title = + params?.title === null ? undefined : params?.title ?? InterpreterQuickPickList.browsePath.openButtonLabel; + const buttons: QuickInputButtonSetup[] = [ + { + button: this.refreshButton, + callback: (quickpickInput) => { + this.refreshCallback(quickpickInput, { isButton: true, showBackButton: params?.showBackButton }); + }, + }, + ]; + if (params?.showBackButton) { + buttons.push({ + button: QuickInputButtons.Back, + callback: () => { + // Do nothing. This is handled as a promise rejection in the quickpick. + }, + }); + } - state.path = undefined; - const selection = await input.showQuickPick< - IInterpreterQuickPickItem | typeof enterInterpreterPathSuggestion, - IQuickPickParameters - >({ - placeholder: InterpreterQuickPickList.quickPickListPlaceholder().format(currentPythonPath), + const selection = await input.showQuickPick>({ + placeholder, items: suggestions, - activeItem: suggestions[1], + sortByLabel: !preserveOrderWhenFiltering, + keepScrollPosition: true, + activeItem: (quickPick) => this.getActiveItem(state.workspace, quickPick), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, - matchOnDescription: true + matchOnDescription: true, + title, + customButtonSetups: buttons, + initialize: (quickPick) => { + // Note discovery is no longer guranteed to be auto-triggered on extension load, so trigger it when + // user interacts with the interpreter picker but only once per session. Users can rely on the + // refresh button if they want to trigger it more than once. However if no envs were found previously, + // always trigger a refresh. + if (this.interpreterService.getInterpreters().length === 0) { + this.refreshCallback(quickPick, { showBackButton: params?.showBackButton }); + } else { + this.refreshCallback(quickPick, { + ifNotTriggerredAlready: true, + showBackButton: params?.showBackButton, + }); + } + }, + onChangeItem: { + event: this.interpreterService.onDidChangeInterpreters, + // It's essential that each callback is handled synchronously, as result of the previous + // callback influences the input for the next one. Input here is the quickpick itself. + callback: (event: PythonEnvironmentsChangedEvent, quickPick) => { + if (this.interpreterService.refreshPromise) { + quickPick.busy = true; + this.interpreterService.refreshPromise.then(() => { + // Items are in the final state as all previous callbacks have finished executing. + quickPick.busy = false; + // Ensure we set a recommended item after refresh has finished. + this.updateQuickPickItems(quickPick, {}, state.workspace, filter, params); + }); + } + this.updateQuickPickItems(quickPick, event, state.workspace, filter, params); + }, + }, }); if (selection === undefined) { - return; - } else if (selection.label === enterInterpreterPathSuggestion.label) { - return this._enterOrBrowseInterpreterPath(input, state); + sendTelemetryEvent(EventName.SELECT_INTERPRETER_SELECTED, undefined, { action: 'escape' }); + } else if (selection.label === this.manualEntrySuggestion.label) { + sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_OR_FIND); + return this._enterOrBrowseInterpreterPath.bind(this); + } else if (selection.label === this.createEnvironmentSuggestion.label) { + const createdEnv = (await Promise.resolve( + this.commandManager.executeCommand(Commands.Create_Environment, { + showBackButton: false, + selectEnvironment: true, + }), + ).catch(noop)) as CreateEnvironmentResult | undefined; + state.path = createdEnv?.path; + } else if (selection.label === this.noPythonInstalled.label) { + this.commandManager.executeCommand(Commands.InstallPython).then(noop, noop); + this.wasNoPythonInstalledItemClicked = true; + } else if (selection.label === this.tipToReloadWindow.label) { + this.commandManager.executeCommand('workbench.action.reloadWindow').then(noop, noop); } else { + sendTelemetryEvent(EventName.SELECT_INTERPRETER_SELECTED, undefined, { action: 'selected' }); state.path = (selection as IInterpreterQuickPickItem).path; } + return undefined; + } + + public _getItems( + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ): QuickPickType[] { + const suggestions: QuickPickType[] = []; + if (params?.showCreateEnvironment) { + suggestions.push(this.createEnvironmentSuggestion, { label: '', kind: QuickPickItemKind.Separator }); + } + + suggestions.push(this.manualEntrySuggestion, { label: '', kind: QuickPickItemKind.Separator }); + + const defaultInterpreterPathSuggestion = this.getDefaultInterpreterPathSuggestion(resource); + if (defaultInterpreterPathSuggestion) { + suggestions.push(defaultInterpreterPathSuggestion); + } + const interpreterSuggestions = this.getSuggestions(resource, filter, params); + this.finalizeItems(interpreterSuggestions, resource, params); + suggestions.push(...interpreterSuggestions); + return suggestions; + } + + private getSuggestions( + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ): QuickPickType[] { + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + const items = this.interpreterSelector + .getSuggestions(resource, !!this.interpreterService.refreshPromise) + .filter((i) => !filter || filter(i.interpreter)); + if (this.interpreterService.refreshPromise) { + // We cannot put items in groups while the list is loading as group of an item can change. + return items; + } + const itemsWithFullName = this.interpreterSelector + .getSuggestions(resource, true) + .filter((i) => !filter || filter(i.interpreter)); + let recommended: IInterpreterQuickPickItem | undefined; + if (!params?.skipRecommended) { + recommended = this.interpreterSelector.getRecommendedSuggestion( + itemsWithFullName, + this.workspaceService.getWorkspaceFolder(resource)?.uri, + ); + } + if (recommended && items[0].interpreter.id === recommended.interpreter.id) { + items.shift(); + } + return getGroupedQuickPickItems(items, recommended, workspaceFolder?.uri.fsPath); + } + + private async getActiveItem(resource: Resource, quickPick: QuickPick) { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const suggestions = quickPick.items; + const activeInterpreterItem = suggestions.find( + (i) => isInterpreterQuickPickItem(i) && i.interpreter.id === interpreter?.id, + ); + if (activeInterpreterItem) { + return activeInterpreterItem; + } + const firstInterpreterSuggestion = suggestions.find((s) => isInterpreterQuickPickItem(s)); + if (firstInterpreterSuggestion) { + return firstInterpreterSuggestion; + } + const noPythonInstalledItem = suggestions.find( + (i) => isSpecialQuickPickItem(i) && i.label === this.noPythonInstalled.label, + ); + return noPythonInstalledItem ?? suggestions[0]; + } + + private getDefaultInterpreterPathSuggestion(resource: Resource): ISpecialQuickPickItem | undefined { + const config = this.workspaceService.getConfiguration('python', resource); + const systemVariables = new SystemVariables(resource, undefined, this.workspaceService); + const defaultInterpreterPathValue = systemVariables.resolveAny(config.get('defaultInterpreterPath')); + if (defaultInterpreterPathValue && defaultInterpreterPathValue !== 'python') { + return { + label: `${Octicons.Gear} ${InterpreterQuickPickList.defaultInterpreterPath.label}`, + description: this.pathUtils.getDisplayName( + defaultInterpreterPathValue, + resource ? resource.fsPath : undefined, + ), + path: defaultInterpreterPathValue, + alwaysShow: true, + }; + } + return undefined; + } + + /** + * Updates quickpick using the change event received. + */ + private updateQuickPickItems( + quickPick: QuickPick, + event: PythonEnvironmentsChangedEvent, + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ) { + // Active items are reset once we replace the current list with updated items, so save it. + const activeItemBeforeUpdate = quickPick.activeItems.length > 0 ? quickPick.activeItems[0] : undefined; + quickPick.items = this.getUpdatedItems(quickPick.items, event, resource, filter, params); + // Ensure we maintain the same active item as before. + const activeItem = activeItemBeforeUpdate + ? quickPick.items.find((item) => { + if (isInterpreterQuickPickItem(item) && isInterpreterQuickPickItem(activeItemBeforeUpdate)) { + return item.interpreter.id === activeItemBeforeUpdate.interpreter.id; + } + if (isSpecialQuickPickItem(item) && isSpecialQuickPickItem(activeItemBeforeUpdate)) { + // 'label' is a constant here instead of 'path'. + return item.label === activeItemBeforeUpdate.label; + } + return false; + }) + : undefined; + if (activeItem) { + quickPick.activeItems = [activeItem]; + } + } + + /** + * Prepare updated items to replace the quickpick list with. + */ + private getUpdatedItems( + items: readonly QuickPickType[], + event: PythonEnvironmentsChangedEvent, + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ): QuickPickType[] { + const updatedItems = [...items.values()]; + const areItemsGrouped = items.find((item) => isSeparatorItem(item)); + const env = event.old ?? event.new; + if (filter && event.new && !filter(event.new)) { + event.new = undefined; // Remove envs we're not looking for from the list. + } + let envIndex = -1; + if (env) { + envIndex = updatedItems.findIndex( + (item) => isInterpreterQuickPickItem(item) && item.interpreter.id === env.id, + ); + } + if (event.new) { + const newSuggestion = this.interpreterSelector.suggestionToQuickPickItem( + event.new, + resource, + !areItemsGrouped, + ); + if (envIndex === -1) { + const noPyIndex = updatedItems.findIndex( + (item) => isSpecialQuickPickItem(item) && item.label === this.noPythonInstalled.label, + ); + if (noPyIndex !== -1) { + updatedItems.splice(noPyIndex, 1); + } + const tryReloadIndex = updatedItems.findIndex( + (item) => isSpecialQuickPickItem(item) && item.label === this.tipToReloadWindow.label, + ); + if (tryReloadIndex !== -1) { + updatedItems.splice(tryReloadIndex, 1); + } + if (areItemsGrouped) { + addSeparatorIfApplicable( + updatedItems, + newSuggestion, + this.workspaceService.getWorkspaceFolder(resource)?.uri.fsPath, + ); + } + updatedItems.push(newSuggestion); + } else { + updatedItems[envIndex] = newSuggestion; + } + } + if (envIndex !== -1 && event.new === undefined) { + updatedItems.splice(envIndex, 1); + } + this.finalizeItems(updatedItems, resource, params); + return updatedItems; + } + + private finalizeItems(items: QuickPickType[], resource: Resource, params?: InterpreterQuickPickParams) { + const interpreterSuggestions = this.interpreterSelector.getSuggestions(resource, true); + const r = this.interpreterService.refreshPromise; + if (!r) { + if (interpreterSuggestions.length) { + if (!params?.skipRecommended) { + this.setRecommendedItem(interpreterSuggestions, items, resource); + } + // Add warning label to certain environments + items.forEach((item, i) => { + if (isInterpreterQuickPickItem(item) && isProblematicCondaEnvironment(item.interpreter)) { + if (!items[i].label.includes(Octicons.Warning)) { + items[i].label = `${Octicons.Warning} ${items[i].label}`; + items[i].tooltip = InterpreterQuickPickList.condaEnvWithoutPythonTooltip; + } + } + }); + } else { + if (!items.some((i) => isSpecialQuickPickItem(i) && i.label === this.noPythonInstalled.label)) { + items.push(this.noPythonInstalled); + } + if ( + this.wasNoPythonInstalledItemClicked && + !items.some((i) => isSpecialQuickPickItem(i) && i.label === this.tipToReloadWindow.label) + ) { + items.push(this.tipToReloadWindow); + } + } + } + } + + private setRecommendedItem( + interpreterSuggestions: IInterpreterQuickPickItem[], + items: QuickPickType[], + resource: Resource, + ) { + const suggestion = this.interpreterSelector.getRecommendedSuggestion( + interpreterSuggestions, + this.workspaceService.getWorkspaceFolder(resource)?.uri, + ); + if (!suggestion) { + return; + } + const areItemsGrouped = items.find((item) => isSeparatorItem(item) && item.label === EnvGroups.Recommended); + const recommended = cloneDeep(suggestion); + recommended.description = areItemsGrouped + ? // No need to add a tag as "Recommended" group already exists. + recommended.description + : `${recommended.description ?? ''} - ${Common.recommended}`; + const index = items.findIndex( + (item) => isInterpreterQuickPickItem(item) && item.interpreter.id === recommended.interpreter.id, + ); + if (index !== -1) { + items[index] = recommended; + } + } + + private refreshCallback( + input: QuickPick, + options?: TriggerRefreshOptions & { isButton?: boolean; showBackButton?: boolean }, + ) { + input.buttons = this.getButtons(options); + + this.interpreterService + .triggerRefresh(undefined, options) + .finally(() => { + input.buttons = this.getButtons({ isButton: false, showBackButton: options?.showBackButton }); + }) + .ignoreErrors(); + if (this.interpreterService.refreshPromise) { + input.busy = true; + this.interpreterService.refreshPromise.then(() => { + input.busy = false; + }); + } + } + + private getButtons(options?: { isButton?: boolean; showBackButton?: boolean }): QuickInputButton[] { + const buttons: QuickInputButton[] = []; + if (options?.showBackButton) { + buttons.push(QuickInputButtons.Back); + } + if (options?.isButton) { + buttons.push({ + iconPath: new ThemeIcon(ThemeIcons.SpinningLoader), + tooltip: InterpreterQuickPickList.refreshingInterpreterList, + }); + } else { + buttons.push(this.refreshButton); + } + return buttons; } @captureTelemetry(EventName.SELECT_INTERPRETER_ENTER_BUTTON) public async _enterOrBrowseInterpreterPath( input: IMultiStepInput, - state: InterpreterStateArgs + state: InterpreterStateArgs, ): Promise> { const items: QuickPickItem[] = [ { - label: InterpreterQuickPickList.browsePath.label(), - detail: InterpreterQuickPickList.browsePath.detail() - } + label: InterpreterQuickPickList.browsePath.label, + detail: InterpreterQuickPickList.browsePath.detail, + }, ]; - const selection = await input.showQuickPick({ - placeholder: InterpreterQuickPickList.enterPath.placeholder(), + const selection = await input.showQuickPick({ + placeholder: InterpreterQuickPickList.enterPath.placeholder, items, - acceptFilterBoxTextAsSelection: true + 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()) { + this.sendInterpreterEntryTelemetry(selection, state.workspace); + } 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(), + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, - title: InterpreterQuickPickList.browsePath.title() + title: InterpreterQuickPickList.browsePath.title, + defaultUri: state.workspace, }); if (uris && uris.length > 0) { state.path = uris[0].fsPath; + this.sendInterpreterEntryTelemetry(state.path!, state.workspace); + } else { + return Promise.reject(InputFlowAction.resume); } } + return Promise.resolve(); } + /** + * @returns true when an interpreter was set, undefined if the user cancelled the quickpick. + */ @captureTelemetry(EventName.SELECT_INTERPRETER) - public async setInterpreter() { - const targetConfig = await this.getConfigTarget(); + public async setInterpreter(options?: { + hideCreateVenv?: boolean; + showBackButton?: boolean; + }): Promise { + const targetConfig = await this.getConfigTargets(); if (!targetConfig) { return; } - const configTarget = targetConfig.configTarget; - const wkspace = targetConfig.folderUri; + const { configTarget } = targetConfig[0]; + const wkspace = targetConfig[0].folderUri; const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this._pickInterpreter(input, s), interpreterState); + try { + await multiStep.run( + (input, s) => + this._pickInterpreter(input, s, undefined, { + showCreateEnvironment: !options?.hideCreateVenv, + showBackButton: options?.showBackButton, + }), + interpreterState, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + // User clicked back button, so we need to return this action. + return { action: 'Back' }; + } + if (ex === InputFlowAction.cancel) { + // User clicked cancel button, so we need to return this action. + return { action: 'Cancel' }; + } + } 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); + if (useEnvExtension()) { + await setInterpreterLegacy(interpreterState.path, wkspace); + } + return { path: interpreterState.path }; } } + + public async getInterpreterViaQuickPick( + workspace: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ): Promise { + const interpreterState: InterpreterStateArgs = { path: undefined, workspace }; + const multiStep = this.multiStepFactory.create(); + await multiStep.run((input, s) => this._pickInterpreter(input, s, filter, params), interpreterState); + return interpreterState.path; + } + + /** + * Check if the interpreter that was entered exists in the list of suggestions. + * If it does, it means that it had already been discovered, + * and we didn't do a good job of surfacing it. + * + * @param selection Intepreter path that was either entered manually or picked by browsing through the filesystem. + */ + // eslint-disable-next-line class-methods-use-this + private sendInterpreterEntryTelemetry(selection: string, workspace: Resource): void { + const suggestions = this._getItems(workspace, undefined); + let interpreterPath = path.normalize(untildify(selection)); + + if (!path.isAbsolute(interpreterPath)) { + interpreterPath = path.resolve(workspace?.fsPath || '', selection); + } + + const expandedPaths = suggestions.map((s) => { + const suggestionPath = isInterpreterQuickPickItem(s) ? s.interpreter.path : ''; + let expandedPath = path.normalize(untildify(suggestionPath)); + + if (!path.isAbsolute(suggestionPath)) { + expandedPath = path.resolve(workspace?.fsPath || '', suggestionPath); + } + + return expandedPath; + }); + + const discovered = expandedPaths.includes(interpreterPath); + + sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTERED_EXISTS, undefined, { discovered }); + + return undefined; + } +} + +function getGroupedQuickPickItems( + items: IInterpreterQuickPickItem[], + recommended: IInterpreterQuickPickItem | undefined, + workspacePath?: string, +): QuickPickType[] { + const updatedItems: QuickPickType[] = []; + if (recommended) { + updatedItems.push({ label: EnvGroups.Recommended, kind: QuickPickItemKind.Separator }, recommended); + } + let previousGroup = EnvGroups.Recommended; + for (const item of items) { + previousGroup = addSeparatorIfApplicable(updatedItems, item, workspacePath, previousGroup); + updatedItems.push(item); + } + return updatedItems; +} + +function addSeparatorIfApplicable( + items: QuickPickType[], + newItem: IInterpreterQuickPickItem, + workspacePath?: string, + previousGroup?: string | undefined, +) { + if (!previousGroup) { + const lastItem = items.length ? items[items.length - 1] : undefined; + previousGroup = + lastItem && isInterpreterQuickPickItem(lastItem) ? getGroup(lastItem, workspacePath) : undefined; + } + const currentGroup = getGroup(newItem, workspacePath); + if (!previousGroup || currentGroup !== previousGroup) { + const separatorItem: QuickPickItem = { label: currentGroup, kind: QuickPickItemKind.Separator }; + items.push(separatorItem); + previousGroup = currentGroup; + } + return previousGroup; } + +function getGroup(item: IInterpreterQuickPickItem, workspacePath?: string) { + if (workspacePath && isParentPath(item.path, workspacePath)) { + return EnvGroups.Workspace; + } + switch (item.interpreter.envType) { + case EnvironmentType.Global: + case EnvironmentType.System: + case EnvironmentType.Unknown: + case EnvironmentType.MicrosoftStore: + return EnvGroups.Global; + default: + return EnvGroups[item.interpreter.envType]; + } +} + +export type SelectEnvironmentResult = { + /** + * Path to the executable python in the environment + */ + readonly path?: string; + /* + * User action that resulted in exit from the create environment flow. + */ + readonly action?: 'Back' | 'Cancel'; +}; diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts deleted file mode 100644 index e3e63b299f83..000000000000 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts +++ /dev/null @@ -1,79 +0,0 @@ -// 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 index 87feb3e599e4..6b33245bb907 100644 --- a/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts @@ -5,10 +5,10 @@ import { inject, injectable } from 'inversify'; import { Disposable, Uri } from 'vscode'; -import { DeprecatePythonPath } from '../../../common/experiments/groups'; -import { IExperimentsManager, IPathUtils, Resource } from '../../../common/types'; -import { PythonInterpreter } from '../../../pythonEnvironments/info'; -import { IInterpreterSecurityService } from '../../autoSelection/types'; +import { arePathsSame, isParentPath } from '../../../common/platform/fs-paths'; +import { IPathUtils, Resource } from '../../../common/types'; +import { getEnvPath } from '../../../pythonEnvironments/base/info/env'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { IInterpreterService } from '../../contracts'; import { IInterpreterComparer, IInterpreterQuickPickItem, IInterpreterSelector } from '../types'; @@ -18,37 +18,63 @@ export class InterpreterSelector implements IInterpreterSelector { 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 + @inject(IInterpreterComparer) private readonly envTypeComparer: IInterpreterComparer, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, ) {} - public dispose() { + + public dispose(): void { 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.filter((item) => this.interpreterSecurityService.isSafe(item) !== false); - } - this.experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - interpreters.sort(this.interpreterComparer.compare.bind(this.interpreterComparer)); + public getSuggestions(resource: Resource, useFullDisplayName = false): IInterpreterQuickPickItem[] { + const interpreters = this.interpreterManager.getInterpreters(resource); + interpreters.sort(this.envTypeComparer.compare.bind(this.envTypeComparer)); + + return interpreters.map((item) => this.suggestionToQuickPickItem(item, resource, useFullDisplayName)); + } + + public async getAllSuggestions(resource: Resource): Promise { + const interpreters = await this.interpreterManager.getAllInterpreters(resource); + interpreters.sort(this.envTypeComparer.compare.bind(this.envTypeComparer)); + return Promise.all(interpreters.map((item) => this.suggestionToQuickPickItem(item, resource))); } - protected async suggestionToQuickPickItem( - suggestion: PythonInterpreter, - workspaceUri?: Uri - ): Promise { - const detail = this.pathUtils.getDisplayName(suggestion.path, workspaceUri ? workspaceUri.fsPath : undefined); - const cachedPrefix = suggestion.cachedEntry ? '(cached) ' : ''; + public suggestionToQuickPickItem( + interpreter: PythonEnvironment, + workspaceUri?: Uri, + useDetailedName = false, + ): IInterpreterQuickPickItem { + if (!useDetailedName) { + const workspacePath = workspaceUri?.fsPath; + if (workspacePath && isParentPath(interpreter.path, workspacePath)) { + // If interpreter is in the workspace, then display the full path. + useDetailedName = true; + } + } + const path = + interpreter.envPath && getEnvPath(interpreter.path, interpreter.envPath).pathType === 'envFolderPath' + ? interpreter.envPath + : interpreter.path; + const detail = this.pathUtils.getDisplayName(path, workspaceUri ? workspaceUri.fsPath : undefined); + const cachedPrefix = interpreter.cachedEntry ? '(cached) ' : ''; return { - // tslint:disable-next-line:no-non-null-assertion - label: suggestion.displayName!, - detail: `${cachedPrefix}${detail}`, - path: suggestion.path, - interpreter: suggestion + label: (useDetailedName ? interpreter.detailedDisplayName : interpreter.displayName) || 'Python', + description: `${cachedPrefix}${detail}`, + path, + interpreter, }; } + + public getRecommendedSuggestion( + suggestions: IInterpreterQuickPickItem[], + resource: Resource, + ): IInterpreterQuickPickItem | undefined { + const envs = this.interpreterManager.getInterpreters(resource); + const recommendedEnv = this.envTypeComparer.getRecommended(envs, resource); + if (!recommendedEnv) { + return undefined; + } + return suggestions.find((item) => arePathsSame(item.interpreter.path, recommendedEnv.path)); + } } diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index 8ad4a38f3155..9814ff6ee4cb 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -1,84 +1,76 @@ 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 { ConfigurationTarget, l10n, Uri, window } from 'vscode'; import { StopWatch } from '../../common/utils/stopWatch'; -import { IServiceContainer } from '../../ioc/types'; -import { InterpreterInformation } from '../../pythonEnvironments/info'; +import { SystemVariables } from '../../common/variables/systemVariables'; +import { traceError } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PythonInterpreterTelemetry } from '../../telemetry/types'; -import { IInterpreterVersionService } from '../contracts'; -import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; +import { IComponentAdapter } from '../contracts'; +import { + IRecommendedEnvironmentService, + 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); - } + constructor( + @inject(IPythonPathUpdaterServiceFactory) + private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory, + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IRecommendedEnvironmentService) private readonly preferredEnvService: IRecommendedEnvironmentService, + ) {} + public async updatePythonPath( pythonPath: string | undefined, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', - wkspace?: Uri + 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) { + await pythonPathUpdater.updatePythonPath(pythonPath); + if (trigger === 'ui') { + this.preferredEnvService.trackUserSelectedEnvironment(pythonPath, wkspace); + } + } catch (err) { failed = true; - // tslint:disable-next-line:no-unsafe-any prefer-type-cast + const reason = err as Error; const message = reason && typeof reason.message === 'string' ? (reason.message as string) : ''; - window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); + window.showErrorMessage(l10n.t('Failed to set interpreter path. Error: {0}', message)); traceError(reason); } // do not wait for this to complete - this.sendTelemetry(stopWatch.elapsedTime, failed, trigger, pythonPath).catch((ex) => - traceError('Python Extension: sendTelemetry', ex) + this.sendTelemetry(stopWatch.elapsedTime, failed, trigger, pythonPath, wkspace).catch((ex) => + traceError('Python Extension: sendTelemetry', ex), ); } + private async sendTelemetry( duration: number, failed: boolean, trigger: 'ui' | 'shebang' | 'load', - pythonPath: string | undefined + pythonPath: string | undefined, + wkspace?: Uri, ) { const telemetryProperties: PythonInterpreterTelemetry = { failed, - trigger + 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; + const systemVariables = new SystemVariables(undefined, wkspace?.fsPath); + const interpreterInfo = await this.pyenvs.getInterpreterInformation(systemVariables.resolveAny(pythonPath)); + if (interpreterInfo) { + telemetryProperties.pythonVersion = interpreterInfo.version?.raw; } } + sendTelemetryEvent(EventName.PYTHON_INTERPRETER, duration, telemetryProperties); } + private getPythonUpdaterService(configTarget: ConfigurationTarget, wkspace?: Uri) { switch (configTarget) { case ConfigurationTarget.Global: { @@ -88,14 +80,14 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage 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 index aecf7beeb5b6..ff42f53bcb5b 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -1,8 +1,6 @@ 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 { IInterpreterPathService } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; @@ -11,37 +9,17 @@ import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './t @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 - ); + return new GlobalPythonPathUpdaterService(this.interpreterPathService); } public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { - return new WorkspacePythonPathUpdaterService( - wkspace, - this.inDeprecatePythonPathExperiment, - this.workspaceService, - this.interpreterPathService - ); + return new WorkspacePythonPathUpdaterService(wkspace, this.interpreterPathService); } public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { - return new WorkspaceFolderPythonPathUpdaterService( - workspaceFolder, - this.inDeprecatePythonPathExperiment, - this.workspaceService, - this.interpreterPathService - ); + return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder, this.interpreterPathService); } } diff --git a/src/client/interpreter/configuration/recommededEnvironmentService.ts b/src/client/interpreter/configuration/recommededEnvironmentService.ts new file mode 100644 index 000000000000..c5356409fcee --- /dev/null +++ b/src/client/interpreter/configuration/recommededEnvironmentService.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IRecommendedEnvironmentService } from './types'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; +import { IExtensionContext, Resource } from '../../common/types'; +import { commands, Uri, workspace } from 'vscode'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../common/persistentState'; +import { traceError } from '../../logging'; +import { IExtensionActivationService } from '../../activation/types'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { isParentPath } from '../../common/platform/fs-paths'; + +const MEMENTO_KEY = 'userSelectedEnvPath'; + +@injectable() +export class RecommendedEnvironmentService implements IRecommendedEnvironmentService, IExtensionActivationService { + private api?: PythonExtension['environments']; + constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { + untrustedWorkspace: true, + virtualWorkspace: false, + }; + + async activate(_resource: Resource, _startupStopWatch?: StopWatch): Promise { + this.extensionContext.subscriptions.push( + commands.registerCommand('python.getRecommendedEnvironment', async (resource: Resource) => { + return this.getRecommededEnvironment(resource); + }), + ); + } + + registerEnvApi(api: PythonExtension['environments']) { + this.api = api; + } + + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined) { + if (workspace.workspaceFolders?.length) { + try { + void updateWorkspaceStateValue(MEMENTO_KEY, getDataToStore(environmentPath, uri)); + } catch (ex) { + traceError('Failed to update workspace state for preferred environment', ex); + } + } else { + void this.extensionContext.globalState.update(MEMENTO_KEY, environmentPath); + } + } + + async getRecommededEnvironment( + resource: Resource, + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + > { + if (!workspace.isTrusted || !this.api) { + return undefined; + } + const preferred = await this.getRecommededInternal(resource); + if (!preferred) { + return undefined; + } + const activeEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)); + const recommendedEnv = await this.api.resolveEnvironment(preferred.environmentPath); + if (activeEnv && recommendedEnv && activeEnv.id !== recommendedEnv.id) { + traceError( + `Active environment ${activeEnv.id} is different from recommended environment ${ + recommendedEnv.id + } for resource ${resource?.toString()}`, + ); + return undefined; + } + if (recommendedEnv) { + return { environment: recommendedEnv, reason: preferred.reason }; + } + const globalEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath()); + if (activeEnv && globalEnv?.path !== activeEnv?.path) { + // User has definitely got a workspace specific environment selected. + // Given the fact that global !== workspace env, we can safely assume that + // at some time, the user has selected a workspace specific environment. + // This applies to cases where the user has selected a workspace specific environment before this version of the extension + // and we did not store it in the workspace state. + // So we can safely return the global environment as the recommended environment. + return { environment: activeEnv, reason: 'workspaceUserSelected' }; + } + return undefined; + } + async getRecommededInternal( + resource: Resource, + ): Promise< + | { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' } + | undefined + > { + let workspaceState: string | undefined = undefined; + try { + workspaceState = getWorkspaceStateValue(MEMENTO_KEY); + } catch (ex) { + traceError('Failed to get workspace state for preferred environment', ex); + } + + if (workspace.workspaceFolders?.length && workspaceState) { + const workspaceUri = ( + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + workspace.workspaceFolders[0].uri + ).toString(); + + try { + const existingJson: Record = JSON.parse(workspaceState); + const selectedEnvPath = existingJson[workspaceUri]; + if (selectedEnvPath) { + return { environmentPath: selectedEnvPath, reason: 'workspaceUserSelected' }; + } + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + } + } + + if (workspace.workspaceFolders?.length && this.api) { + // Check if we have a .venv or .conda environment in the workspace + // This is required for cases where user has selected a workspace specific environment + // but before this version of the extension, we did not store it in the workspace state. + const workspaceEnv = await getWorkspaceSpecificVirtualEnvironment(this.api, resource); + if (workspaceEnv) { + return { environmentPath: workspaceEnv.path, reason: 'workspaceUserSelected' }; + } + } + + const globalSelectedEnvPath = this.extensionContext.globalState.get(MEMENTO_KEY); + if (globalSelectedEnvPath) { + return { environmentPath: globalSelectedEnvPath, reason: 'globalUserSelected' }; + } + return this.api && workspace.isTrusted + ? { + environmentPath: this.api.getActiveEnvironmentPath(resource).path, + reason: 'defaultRecommended', + } + : undefined; + } +} + +async function getWorkspaceSpecificVirtualEnvironment(api: PythonExtension['environments'], resource: Resource) { + const workspaceUri = + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + (workspace.workspaceFolders?.length ? workspace.workspaceFolders[0].uri : undefined); + if (!workspaceUri) { + return undefined; + } + let workspaceEnv = api.known.find((env) => { + if (!env.environment?.folderUri) { + return false; + } + if (env.environment.type !== 'VirtualEnvironment' && env.environment.type !== 'Conda') { + return false; + } + return isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath); + }); + let resolvedEnv = workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; + if (resolvedEnv) { + return resolvedEnv; + } + workspaceEnv = api.known.find((env) => { + // Look for any other type of env thats inside this workspace + // Or look for an env thats associated with this workspace (pipenv or the like). + return ( + (env.environment?.folderUri && isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath)) || + (env.environment?.workspaceFolder && env.environment.workspaceFolder.uri.fsPath === workspaceUri.fsPath) + ); + }); + return workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; +} + +function getDataToStore(environmentPath: string | undefined, uri: Uri | undefined): string | undefined { + if (!workspace.workspaceFolders?.length) { + return environmentPath; + } + const workspaceUri = ( + (uri ? workspace.getWorkspaceFolder(uri)?.uri : undefined) || workspace.workspaceFolders[0].uri + ).toString(); + const existingData = getWorkspaceStateValue(MEMENTO_KEY); + if (!existingData) { + return JSON.stringify(environmentPath ? { [workspaceUri]: environmentPath } : {}); + } + try { + const existingJson: Record = JSON.parse(existingData); + if (environmentPath) { + existingJson[workspaceUri] = environmentPath; + } else { + delete existingJson[workspaceUri]; + } + return JSON.stringify(existingJson); + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + return JSON.stringify({ + [workspaceUri]: environmentPath, + }); + } +} diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts index 39a1c1731019..1cf2a7cc478f 100644 --- a/src/client/interpreter/configuration/services/globalUpdaterService.ts +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -1,27 +1,15 @@ 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 - ) {} + constructor(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')!; + const pythonPathValue = this.interpreterPathService.inspect(undefined); if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { return; } - if (this.inDeprecatePythonPathExperiment) { - await this.interpreterPathService.update(undefined, ConfigurationTarget.Global, pythonPath); - } else { - await pythonConfig.update('pythonPath', pythonPath, true); - } + await this.interpreterPathService.update(undefined, ConfigurationTarget.Global, pythonPath); } } diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts index 3e7888bab2b3..8c9656b3febf 100644 --- a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -1,36 +1,15 @@ -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 - ) {} + constructor(private workspaceFolder: Uri, 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')!; + const pythonPathValue = this.interpreterPathService.inspect(this.workspaceFolder); 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); - } + await this.interpreterPathService.update(this.workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath); } } diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts index 198eaf5f59cb..65bcd0b30e39 100644 --- a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -1,32 +1,15 @@ -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 - ) {} + constructor(private workspace: Uri, 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')!; + const pythonPathValue = this.interpreterPathService.inspect(this.workspace); 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); - } + await this.interpreterPathService.update(this.workspace, ConfigurationTarget.Workspace, pythonPath); } } diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index a2c96e32147c..05ff8e32c18e 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -1,6 +1,7 @@ import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; import { Resource } from '../../common/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; export interface IPythonPathUpdaterService { updatePythonPath(pythonPath: string | undefined): Promise; @@ -19,13 +20,26 @@ export interface IPythonPathUpdaterServiceManager { pythonPath: string | undefined, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', - wkspace?: Uri + wkspace?: Uri, ): Promise; } export const IInterpreterSelector = Symbol('IInterpreterSelector'); export interface IInterpreterSelector extends Disposable { - getSuggestions(resource: Resource): Promise; + getRecommendedSuggestion( + suggestions: IInterpreterQuickPickItem[], + resource: Resource, + ): IInterpreterQuickPickItem | undefined; + /** + * @deprecated Only exists for old Jupyter integration. + */ + getAllSuggestions(resource: Resource): Promise; + getSuggestions(resource: Resource, useFullDisplayName?: boolean): IInterpreterQuickPickItem[]; + suggestionToQuickPickItem( + suggestion: PythonEnvironment, + workspaceUri?: Uri | undefined, + useDetailedName?: boolean, + ): IInterpreterQuickPickItem; } export interface IInterpreterQuickPickItem extends QuickPickItem { @@ -33,13 +47,68 @@ export interface IInterpreterQuickPickItem extends QuickPickItem { /** * The interpreter related to this quickpick item. * - * @type {PythonInterpreter} + * @type {PythonEnvironment} * @memberof IInterpreterQuickPickItem */ - interpreter: PythonInterpreter; + interpreter: PythonEnvironment; +} + +export interface ISpecialQuickPickItem extends QuickPickItem { + path?: string; } export const IInterpreterComparer = Symbol('IInterpreterComparer'); export interface IInterpreterComparer { - compare(a: PythonInterpreter, b: PythonInterpreter): number; + initialize(resource: Resource): Promise; + compare(a: PythonEnvironment, b: PythonEnvironment): number; + getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined; +} + +export interface InterpreterQuickPickParams { + /** + * Specify `null` if a placeholder is not required. + */ + placeholder?: string | null; + /** + * Specify `null` if a title is not required. + */ + title?: string | null; + /** + * Specify `true` to skip showing recommended python interpreter. + */ + skipRecommended?: boolean; + + /** + * Specify `true` to show back button. + */ + showBackButton?: boolean; + + /** + * Show button to create a new environment. + */ + showCreateEnvironment?: boolean; +} + +export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick'); +export interface IInterpreterQuickPick { + getInterpreterViaQuickPick( + workspace: Resource, + filter?: (i: PythonEnvironment) => boolean, + params?: InterpreterQuickPickParams, + ): Promise; +} + +export const IRecommendedEnvironmentService = Symbol('IRecommendedEnvironmentService'); +export interface IRecommendedEnvironmentService { + registerEnvApi(api: PythonExtension['environments']): void; + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined): void; + getRecommededEnvironment( + resource: Resource, + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; } diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index c2ab3dfc6ff6..30a05c140249 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,128 +1,114 @@ import { SemVer } from 'semver'; -import { CodeLensProvider, Disposable, Event, TextDocument, Uri } from 'vscode'; +import { ConfigurationTarget, Disposable, Event, Uri } from 'vscode'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; import { Resource } from '../common/types'; -import { CondaEnvironmentInfo, CondaInfo } from '../pythonEnvironments/discovery/locators/services/conda'; -import { GetInterpreterLocatorOptions } from '../pythonEnvironments/discovery/locators/types'; -import { InterpreterType, PythonInterpreter } 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'); +import { PythonEnvSource } from '../pythonEnvironments/base/info'; +import { + GetRefreshEnvironmentsOptions, + ProgressNotificationEvent, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../pythonEnvironments/base/locator'; +import { CondaEnvironmentInfo, CondaInfo } from '../pythonEnvironments/common/environmentManagers/conda'; +import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; + +export type PythonEnvironmentsChangedEvent = { + type?: FileChangeType; + resource?: Uri; + old?: PythonEnvironment; + new?: PythonEnvironment | undefined; +}; + +export const IComponentAdapter = Symbol('IComponentAdapter'); +export interface IComponentAdapter { + readonly onProgress: Event; + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + readonly onChanged: Event; + // VirtualEnvPrompt + onDidCreate(resource: Resource, callback: () => void): Disposable; + // IInterpreterLocatorService + hasInterpreters(filter?: (e: PythonEnvironment) => Promise): Promise; + getInterpreters(resource?: Uri, source?: PythonEnvSource[]): PythonEnvironment[]; + + // WorkspaceVirtualEnvInterpretersAutoSelectionRule + getWorkspaceVirtualEnvInterpreters( + resource: Uri, + options?: { ignoreCache?: boolean }, + ): Promise; + + // IInterpreterService + getInterpreterDetails(pythonPath: string): Promise; + + // IInterpreterHelper + // Undefined is expected on this API, if the environment info retrieval fails. + getInterpreterInformation(pythonPath: string): Promise | undefined>; + + isMacDefaultPythonPath(pythonPath: string): Promise; + + // ICondaService + isCondaEnvironment(interpreterPath: string): Promise; + // Undefined is expected on this API, if the environment is not conda env. + getCondaEnvironment(interpreterPath: string): Promise; -export interface IInterpreterLocatorService extends Disposable { - readonly onLocating: Event>; - readonly hasInterpreters: Promise; - didTriggerInterpreterSuggestions?: boolean; - getInterpreters(resource?: Uri, options?: GetInterpreterLocatorOptions): Promise; + isMicrosoftStoreInterpreter(pythonPath: string): Promise; } export const ICondaService = Symbol('ICondaService'); - +/** + * Interface carries the properties which are not available via the discovery component interface. + */ export interface ICondaService { - readonly condaEnvironmentsFile: string | undefined; - getCondaFile(): Promise; + getCondaFile(forShellExecution?: boolean): Promise; + getCondaInfo(): Promise; isCondaAvailable(): Promise; getCondaVersion(): Promise; - getCondaInfo(): Promise; - getCondaEnvironments(ignoreCache: boolean): Promise; - getInterpreterPath(condaEnvironmentPath: string): string; + getInterpreterPathForEnvironment(condaEnv: CondaEnvironmentInfo): Promise; getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise; - isCondaEnvironment(interpreterPath: string): Promise; - getCondaEnvironment(interpreterPath: string): Promise; + getActivationScriptFromInterpreter( + interpreterPath?: string, + envName?: string, + ): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined>; } export const IInterpreterService = Symbol('IInterpreterService'); export interface IInterpreterService { + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; + readonly refreshPromise: Promise | undefined; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + readonly onDidChangeInterpreters: Event; onDidChangeInterpreterConfiguration: Event; - onDidChangeInterpreter: Event; - onDidChangeInterpreterInformation: Event; - hasInterpreters: Promise; - getInterpreters(resource?: Uri, options?: GetInterpreterOptions): Promise; - getActiveInterpreter(resource?: Uri): Promise; - getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise; + onDidChangeInterpreter: Event; + onDidChangeInterpreterInformation: Event; + /** + * Note this API does not trigger the refresh but only works with the current refresh if any. Information + * returned by this is more or less upto date but is not guaranteed to be. + */ + hasInterpreters(filter?: (e: PythonEnvironment) => Promise): Promise; + getInterpreters(resource?: Uri): PythonEnvironment[]; + /** + * @deprecated Only exists for old Jupyter integration. + */ + getAllInterpreters(resource?: Uri): 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; + registerVisibilityFilter(filter: IInterpreterStatusbarVisibilityFilter): void; } export const IInterpreterHelper = Symbol('IInterpreterHelper'); export interface IInterpreterHelper { getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined; - getInterpreterInformation(pythonPath: string): Promise>; - isMacDefaultPythonPath(pythonPath: string): Boolean; - getInterpreterTypeDisplayName(interpreterType: InterpreterType): string | undefined; - getBestInterpreter(interpreters?: PythonInterpreter[]): PythonInterpreter | undefined; -} - -export const IPipEnvService = Symbol('IPipEnvService'); -export interface IPipEnvService { - executable: string; - isRelatedPipEnvironment(dir: string, pythonPath: string): Promise; -} - -export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper'); -export interface IInterpreterLocatorHelper { - mergeInterpreters(interpreters: PythonInterpreter[]): Promise; -} - -export interface IInterpreterWatcher { - onDidCreate: Event; -} - -export const IInterpreterWatcherRegistry = Symbol('IInterpreterWatcherRegistry'); -export interface IInterpreterWatcherRegistry extends IInterpreterWatcher { - register(resource: Resource): Promise; -} - -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; + getInterpreterInformation(pythonPath: string): Promise>; + isMacDefaultPythonPath(pythonPath: string): Promise; + getInterpreterTypeDisplayName(interpreterType: EnvironmentType): string | undefined; + getBestInterpreter(interpreters?: PythonEnvironment[]): PythonEnvironment | undefined; } export const IInterpreterStatusbarVisibilityFilter = Symbol('IInterpreterStatusbarVisibilityFilter'); @@ -131,5 +117,15 @@ export const IInterpreterStatusbarVisibilityFilter = Symbol('IInterpreterStatusb */ export interface IInterpreterStatusbarVisibilityFilter { readonly changed?: Event; - readonly visible: boolean; + readonly hidden: boolean; +} + +export type WorkspacePythonPath = { + folderUri: Uri; + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; +}; + +export const IActivatedEnvironmentLaunch = Symbol('IActivatedEnvironmentLaunch'); +export interface IActivatedEnvironmentLaunch { + selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection?: boolean): Promise; } diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 878d7e36e250..3a602093d4f9 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,71 +1,96 @@ -import { inject, injectable, multiInject } from 'inversify'; -import { Disposable, OutputChannel, StatusBarAlignment, StatusBarItem, Uri } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { + Disposable, + l10n, + LanguageStatusItem, + LanguageStatusSeverity, + StatusBarAlignment, + StatusBarItem, + ThemeColor, + Uri, +} from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; +import { Commands, PYTHON_LANGUAGE } from '../../common/constants'; import '../../common/extensions'; -import { IDisposableRegistry, IOutputChannel, IPathUtils, Resource } from '../../common/types'; -import { Interpreters } from '../../common/utils/localize'; +import { IDisposableRegistry, IPathUtils, Resource } from '../../common/types'; +import { InterpreterQuickPickList, Interpreters } from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; -import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { IInterpreterAutoSelectionService } from '../autoSelection/types'; +import { traceLog } from '../../logging'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, - IInterpreterStatusbarVisibilityFilter + IInterpreterStatusbarVisibilityFilter, } from '../contracts'; +import { shouldEnvExtHandleActivation } from '../../envExt/api.internal'; /** - * 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. + * Based on https://github.com/microsoft/vscode-python/issues/18040#issuecomment-992567670. + * This is to ensure the item appears right after the Python language status item. */ +const STATUS_BAR_ITEM_PRIORITY = 100.09999; + @injectable() -export class AlwaysDisplayStatusBar implements IInterpreterStatusbarVisibilityFilter { - public get visible(): boolean { - return true; - } -} -// tslint:disable-next-line:completed-docs -@injectable() -export class InterpreterDisplay implements IInterpreterDisplay { - private readonly statusBar: StatusBarItem; +export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingleActivationService { + public supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { + untrustedWorkspace: false, + virtualWorkspace: true, + }; + private statusBar: StatusBarItem | undefined; + private useLanguageStatus = false; + private languageStatus: LanguageStatusItem | undefined; private readonly helper: IInterpreterHelper; private readonly workspaceService: IWorkspaceService; private readonly pathUtils: IPathUtils; private readonly interpreterService: IInterpreterService; + private currentlySelectedInterpreterDisplay?: string; private currentlySelectedInterpreterPath?: string; private currentlySelectedWorkspaceFolder: Resource; - private readonly autoSelection: IInterpreterAutoSelectionService; - private interpreterPath: string | undefined; private statusBarCanBeDisplayed?: boolean; + private visibilityFilters: IInterpreterStatusbarVisibilityFilter[] = []; + private disposableRegistry: Disposable[]; - constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @multiInject(IInterpreterStatusbarVisibilityFilter) - private readonly visibilityFilters: IInterpreterStatusbarVisibilityFilter[] - ) { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { 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.disposableRegistry = serviceContainer.get(IDisposableRegistry); this.interpreterService.onDidChangeInterpreterInformation( this.onDidChangeInterpreterInformation, this, - disposableRegistry + this.disposableRegistry, ); - this.visibilityFilters - .filter((item) => item.changed) - .forEach((item) => item.changed!(this.updateVisibility, this, disposableRegistry)); } + + public async activate(): Promise { + if (shouldEnvExtHandleActivation()) { + return; + } + const application = this.serviceContainer.get(IApplicationShell); + if (this.useLanguageStatus) { + this.languageStatus = application.createLanguageStatusItem('python.selectedInterpreter', { + language: PYTHON_LANGUAGE, + }); + this.languageStatus.severity = LanguageStatusSeverity.Information; + this.languageStatus.command = { + title: InterpreterQuickPickList.browsePath.openButtonLabel, + command: Commands.Set_Interpreter, + }; + this.disposableRegistry.push(this.languageStatus); + } else { + const [alignment, priority] = [StatusBarAlignment.Right, STATUS_BAR_ITEM_PRIORITY]; + this.statusBar = application.createStatusBarItem(alignment, priority, 'python.selectedInterpreterDisplay'); + this.statusBar.command = Commands.Set_Interpreter; + this.disposableRegistry.push(this.statusBar); + this.statusBar.name = Interpreters.selectedPythonInterpreter; + } + } + public async refresh(resource?: Uri) { // Use the workspace Uri if available if (resource && this.workspaceService.getWorkspaceFolder(resource)) { @@ -77,43 +102,91 @@ export class InterpreterDisplay implements IInterpreterDisplay { } await this.updateDisplay(resource); } - private onDidChangeInterpreterInformation(info: PythonInterpreter) { - if (!this.currentlySelectedInterpreterPath || this.currentlySelectedInterpreterPath === info.path) { + public registerVisibilityFilter(filter: IInterpreterStatusbarVisibilityFilter) { + const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); + this.visibilityFilters.push(filter); + if (filter.changed) { + filter.changed(this.updateVisibility, this, disposableRegistry); + } + } + private onDidChangeInterpreterInformation(info: PythonEnvironment) { + if (this.currentlySelectedInterpreterPath === info.path) { this.updateDisplay(this.currentlySelectedWorkspaceFolder).ignoreErrors(); } } private async updateDisplay(workspaceFolder?: Uri) { - await this.autoSelection.autoSelectInterpreter(workspaceFolder); + if (shouldEnvExtHandleActivation()) { + this.statusBar?.hide(); + this.languageStatus?.dispose(); + this.languageStatus = undefined; + return; + } const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); + if ( + this.currentlySelectedInterpreterDisplay && + this.currentlySelectedInterpreterDisplay === interpreter?.detailedDisplayName && + this.currentlySelectedInterpreterPath === interpreter.path + ) { + return; + } 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; + if (this.statusBar) { + if (interpreter) { + this.statusBar.color = ''; + this.statusBar.tooltip = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); + if (this.currentlySelectedInterpreterPath !== interpreter.path) { + traceLog( + l10n.t( + 'Python interpreter path: {0}', + this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath), + ), + ); + this.currentlySelectedInterpreterPath = interpreter.path; + } + let text = interpreter.detailedDisplayName; + text = text?.startsWith('Python') ? text?.substring('Python'.length)?.trim() : text; + this.statusBar.text = text ?? ''; + this.statusBar.backgroundColor = undefined; + this.currentlySelectedInterpreterDisplay = interpreter.detailedDisplayName; + } else { + this.statusBar.tooltip = ''; + this.statusBar.color = ''; + this.statusBar.backgroundColor = new ThemeColor('statusBarItem.warningBackground'); + this.statusBar.text = `$(alert) ${InterpreterQuickPickList.browsePath.openButtonLabel}`; + this.currentlySelectedInterpreterDisplay = undefined; + } + } else if (this.languageStatus) { + if (interpreter) { + this.languageStatus.detail = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); + if (this.currentlySelectedInterpreterPath !== interpreter.path) { + traceLog( + l10n.t( + 'Python interpreter path: {0}', + this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath), + ), + ); + this.currentlySelectedInterpreterPath = interpreter.path; + } + let text = interpreter.detailedDisplayName!; + text = text.startsWith('Python') ? text.substring('Python'.length).trim() : text; + this.languageStatus.text = text; + this.currentlySelectedInterpreterDisplay = interpreter.detailedDisplayName; + this.languageStatus.severity = LanguageStatusSeverity.Information; + } else { + this.languageStatus.severity = LanguageStatusSeverity.Warning; + this.languageStatus.text = `$(alert) ${InterpreterQuickPickList.browsePath.openButtonLabel}`; + this.languageStatus.detail = undefined; + this.currentlySelectedInterpreterDisplay = undefined; } - 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) { + if (!this.statusBar || !this.statusBarCanBeDisplayed) { return; } - if (this.visibilityFilters.length === 0 || this.visibilityFilters.every((filter) => filter.visible)) { + 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 deleted file mode 100644 index d03191d335f6..000000000000 --- a/src/client/interpreter/display/interpreterSelectionTip.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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 { IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { Common, Interpreters } from '../../common/utils/localize'; - -@injectable() -export class InterpreterSelectionTip implements IExtensionSingleActivationService { - private readonly storage: IPersistentState; - constructor( - @inject(IApplicationShell) private readonly shell: IApplicationShell, - @inject(IPersistentStateFactory) private readonly factory: IPersistentStateFactory - ) { - this.storage = this.factory.createGlobalPersistentState('InterpreterSelectionTip', false); - } - public async activate(): Promise { - if (this.storage.value) { - return; - } - this.showTip().ignoreErrors(); - } - @swallowExceptions('Failed to display tip') - private async showTip() { - const selection = await this.shell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt()); - if (selection !== Common.gotIt()) { - return; - } - await this.storage.updateValue(true); - } -} diff --git a/src/client/interpreter/display/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts index 83b5b9dfbe30..4b2811043d2f 100644 --- a/src/client/interpreter/display/progressDisplay.ts +++ b/src/client/interpreter/display/progressDisplay.ts @@ -5,44 +5,70 @@ import { inject, injectable } from 'inversify'; import { Disposable, ProgressLocation, ProgressOptions } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; import { IApplicationShell } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; +import { Commands } from '../../common/constants'; import { IDisposableRegistry } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; -import { Common, Interpreters } from '../../common/utils/localize'; -import { IInterpreterLocatorProgressHandler, IInterpreterLocatorProgressService } from '../contracts'; +import { Interpreters } from '../../common/utils/localize'; +import { traceDecoratorVerbose } from '../../logging'; +import { ProgressReportStage } from '../../pythonEnvironments/base/locator'; +import { IComponentAdapter } from '../contracts'; +// The parts of IComponentAdapter used here. @injectable() -export class InterpreterLocatorProgressStatubarHandler implements IInterpreterLocatorProgressHandler { +export class InterpreterLocatorProgressStatusBarHandler implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + 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[] + @inject(IDisposableRegistry) private readonly disposables: Disposable[], + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, ) {} - public register() { - this.progressService.onRefreshing(() => this.showProgress(), this, this.disposables); - this.progressService.onRefreshed(() => this.hideProgress(), this, this.disposables); + + public async activate(): Promise { + this.pyenvs.onProgress( + (event) => { + if (event.stage === ProgressReportStage.discoveryStarted) { + this.showProgress(); + const refreshPromise = this.pyenvs.getRefreshPromise(); + if (refreshPromise) { + refreshPromise.then(() => this.hideProgress()); + } + } else if (event.stage === ProgressReportStage.discoveryFinished) { + this.hideProgress(); + } + }, + this, + this.disposables, + ); } - @traceDecorators.verbose('Display locator refreshing progress') + + @traceDecoratorVerbose('Display locator refreshing progress') private showProgress(): void { if (!this.deferred) { this.createProgress(); } } - @traceDecorators.verbose('Hide locator refreshing progress') + + @traceDecoratorVerbose('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() + title: `[${ + this.isFirstTimeLoadingInterpreters ? Interpreters.discovering : Interpreters.refreshing + }](command:${Commands.Set_Interpreter})`, }; this.isFirstTimeLoadingInterpreters = false; this.shell.withProgress(progressOptions, () => { diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts deleted file mode 100644 index 8737981c8369..000000000000 --- a/src/client/interpreter/display/shebangCodeLensProvider.ts +++ /dev/null @@ -1,86 +0,0 @@ -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 index 1eb9c8ccd18d..413fa225f3ef 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,60 +1,47 @@ 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 { Resource } from '../common/types'; import { IServiceContainer } from '../ioc/types'; -import { isMacDefaultPythonPath } from '../pythonEnvironments/discovery'; -import { - getInterpreterTypeName, - InterpreterInformation, - InterpreterType, - PythonInterpreter, - sortInterpreters -} from '../pythonEnvironments/info'; -import { IInterpreterHelper } from './contracts'; -import { IInterpreterHashProviderFactory } from './locators/types'; +import { PythonEnvSource } from '../pythonEnvironments/base/info'; +import { compareSemVerLikeVersions } from '../pythonEnvironments/base/info/pythonVersion'; +import { EnvironmentType, getEnvironmentTypeName, PythonEnvironment } from '../pythonEnvironments/info'; +import { IComponentAdapter, IInterpreterHelper, WorkspacePythonPath } from './contracts'; -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: PythonInterpreter, activeWorkspaceUri: Uri) { +export function isInterpreterLocatedInWorkspace(interpreter: PythonEnvironment, activeWorkspaceUri: Uri): boolean { const fileSystemPaths = FileSystemPaths.withDefaults(); const interpreterPath = fileSystemPaths.normCase(interpreter.path); const resourcePath = fileSystemPaths.normCase(activeWorkspaceUri.fsPath); return interpreterPath.startsWith(resourcePath); } +/** + * 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 ? compareSemVerLikeVersions(a.version, b.version) : 0)); + return sorted; +} + @injectable() export class InterpreterHelper implements IInterpreterHelper { - private readonly persistentFactory: IPersistentStateFactory; constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IInterpreterHashProviderFactory) private readonly hashProviderFactory: IInterpreterHashProviderFactory - ) { - this.persistentFactory = this.serviceContainer.get(IPersistentStateFactory); - } + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + ) {} + public getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined { const workspaceService = this.serviceContainer.get(IWorkspaceService); - if (!workspaceService.hasWorkspaceFolders) { + const hasWorkspaceFolders = (workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { return; } if (Array.isArray(workspaceService.workspaceFolders) && workspaceService.workspaceFolders.length === 1) { @@ -76,51 +63,36 @@ export class InterpreterHelper implements IInterpreterHelper { } } } - 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 async getInterpreterInformation(pythonPath: string): Promise> { + return this.pyenvs.getInterpreterInformation(pythonPath); + } + + public async getInterpreters({ resource, source }: { resource?: Uri; source?: PythonEnvSource[] } = {}): Promise< + PythonEnvironment[] + > { + const interpreters = await this.pyenvs.getInterpreters(resource, source); + return sortInterpreters(interpreters); + } + + public async getInterpreterPath(pythonPath: string): Promise { + const interpreterInfo: any = await this.getInterpreterInformation(pythonPath); + if (interpreterInfo) { + return interpreterInfo.path; + } else { + return pythonPath; } } - public isMacDefaultPythonPath(pythonPath: string) { - return isMacDefaultPythonPath(pythonPath); + + public async isMacDefaultPythonPath(pythonPath: string): Promise { + return this.pyenvs.isMacDefaultPythonPath(pythonPath); } - public getInterpreterTypeDisplayName(interpreterType: InterpreterType) { - return getInterpreterTypeName(interpreterType); + + public getInterpreterTypeDisplayName(interpreterType: EnvironmentType): string { + return getEnvironmentTypeName(interpreterType); } - public getBestInterpreter(interpreters?: PythonInterpreter[]): PythonInterpreter | undefined { + + public getBestInterpreter(interpreters?: PythonEnvironment[]): PythonEnvironment | undefined { if (!Array.isArray(interpreters) || interpreters.length === 0) { return; } diff --git a/src/client/interpreter/interpreterPathCommand.ts b/src/client/interpreter/interpreterPathCommand.ts new file mode 100644 index 000000000000..12f6756dafeb --- /dev/null +++ b/src/client/interpreter/interpreterPathCommand.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri, workspace } from 'vscode'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { Commands } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { registerCommand } from '../common/vscodeApis/commandApis'; +import { IInterpreterService } from './contracts'; +import { useEnvExtension } from '../envExt/api.internal'; + +@injectable() +export class InterpreterPathCommand implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push( + registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), + ); + } + + public async _getSelectedInterpreterPath( + args: { workspaceFolder: string; type: string } | string[], + ): Promise { + // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder + // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder + let workspaceFolder; + if ('workspaceFolder' in args) { + workspaceFolder = args.workspaceFolder; + } else if (args[1]) { + const [, second] = args; + workspaceFolder = second; + } else if (useEnvExtension() && 'type' in args && args.type === 'debugpy') { + // If using the envsExt and the type is debugpy, we need to add the workspace folder to get the interpreter path. + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolder = workspace.workspaceFolders[0].uri.fsPath; + } + } else { + workspaceFolder = undefined; + } + + let workspaceFolderUri; + try { + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; + } catch (ex) { + workspaceFolderUri = undefined; + } + + const interpreterPath = + (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; + return interpreterPath.toCommandArgumentForPythonExt(); + } +} diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index d268ec8a58f1..ad06fd7d051d 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -1,357 +1,322 @@ +// eslint-disable-next-line max-classes-per-file 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 * as pathUtils from 'path'; +import { + ConfigurationChangeEvent, + Disposable, + Event, + EventEmitter, + ProgressLocation, + ProgressOptions, + Uri, + WorkspaceFolder, +} from 'vscode'; +import '../common/extensions'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { IConfigurationService, IDisposableRegistry, - IExperimentsManager, + IInstaller, IInterpreterPathService, - IPersistentState, - IPersistentStateFactory, - Resource + Product, } from '../common/types'; -import { sleep } from '../common/utils/async'; import { IServiceContainer } from '../ioc/types'; -import { InterpreterType, PythonInterpreter } from '../pythonEnvironments/info'; -import { captureTelemetry } from '../telemetry'; -import { EventName } from '../telemetry/constants'; +import { PythonEnvironment } from '../pythonEnvironments/info'; import { + IActivatedEnvironmentLaunch, + IComponentAdapter, IInterpreterDisplay, - IInterpreterHelper, - IInterpreterLocatorService, IInterpreterService, - INTERPRETER_LOCATOR_SERVICE + IInterpreterStatusbarVisibilityFilter, + PythonEnvironmentsChangedEvent, } from './contracts'; -import { IInterpreterHashProviderFactory } from './locators/types'; -import { IVirtualEnvironmentManager } from './virtualEnvs/types'; - -const EXPITY_DURATION = 24 * 60 * 60 * 1000; +import { traceError, traceLog } from '../logging'; +import { Commands, PVSC_EXTENSION_ID, PYTHON_LANGUAGE } from '../common/constants'; +import { reportActiveInterpreterChanged } from '../environmentApi'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { Interpreters } from '../common/utils/localize'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { cache } from '../common/utils/decorators'; +import { + GetRefreshEnvironmentsOptions, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../pythonEnvironments/base/locator'; +import { sleep } from '../common/utils/async'; +import { useEnvExtension } from '../envExt/api.internal'; +import { getActiveInterpreterLegacy } from '../envExt/api.legacy'; -export type GetInterpreterOptions = { - onSuggestion?: boolean; -}; +type StoredPythonEnvironment = PythonEnvironment & { store?: boolean }; @injectable() export class InterpreterService implements Disposable, IInterpreterService { - public get hasInterpreters(): Promise { - return this.locator.hasInterpreters; + public async hasInterpreters( + filter: (e: PythonEnvironment) => Promise = async () => true, + ): Promise { + return this.pyenvs.hasInterpreters(filter); + } + + public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise { + return this.pyenvs.triggerRefresh(query, options); } - public get onDidChangeInterpreter(): Event { + public get refreshPromise(): Promise | undefined { + return this.pyenvs.getRefreshPromise(); + } + + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this.pyenvs.getRefreshPromise(options); + } + + public get onDidChangeInterpreter(): Event { return this.didChangeInterpreterEmitter.event; } - public get onDidChangeInterpreterInformation(): Event { + public onDidChangeInterpreters: Event; + + public get onDidChangeInterpreterInformation(): Event { return this.didChangeInterpreterInformation.event; } + public get onDidChangeInterpreterConfiguration(): Event { return this.didChangeInterpreterConfigurationEmitter.event; } - public _pythonPathSetting: string = ''; + + public _pythonPathSetting: string | undefined = ''; + 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(); + + private readonly didChangeInterpreterEmitter = new EventEmitter(); + + private readonly didChangeInterpreterInformation = new EventEmitter(); + + private readonly activeInterpreterPaths = new Map< + string, + { path: string; workspaceFolder: WorkspaceFolder | undefined } + >(); constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IInterpreterHashProviderFactory) private readonly hashProviderFactory: IInterpreterHashProviderFactory + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, ) { - 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); + this.onDidChangeInterpreters = pyenvs.onChanged; } - public async refresh(resource?: Uri) { + public async refresh(resource?: Uri): Promise { const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); - return interpreterDisplay.refresh(resource); + await interpreterDisplay.refresh(resource); + const workspaceFolder = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolder(resource); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); } - public initialize() { + public initialize(): void { 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 interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + const filter = new (class implements IInterpreterStatusbarVisibilityFilter { + constructor( + private readonly docManager: IDocumentManager, + private readonly configService: IConfigurationService, + private readonly disposablesReg: IDisposableRegistry, + ) { + this.disposablesReg.push( + this.configService.onDidChange(async (event: ConfigurationChangeEvent | undefined) => { + if (event?.affectsConfiguration('python.interpreter.infoVisibility')) { + this.interpreterVisibilityEmitter.fire(); + } + }), ); - const workspaceUri = workspaceUriIndex === -1 ? undefined : workspacesUris[workspaceUriIndex]; - this._onConfigChanged(workspaceUri); - }); - disposables.push(disposable); - } - this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - } + } + + public readonly interpreterVisibilityEmitter = new EventEmitter(); + + public readonly changed = this.interpreterVisibilityEmitter.event; - @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(); + get hidden() { + const visibility = this.configService.getSettings().interpreter.infoVisibility; + if (visibility === 'never') { + return true; + } + if (visibility === 'always') { + return false; + } + const document = this.docManager.activeTextEditor?.document; + // Output channel for MS Python related extensions. These contain "ms-python" in their ID. + const pythonOutputChannelPattern = PVSC_EXTENSION_ID.split('.')[0]; + if ( + document?.fileName.endsWith('settings.json') || + document?.fileName.includes(pythonOutputChannelPattern) + ) { + return false; + } + return document?.languageId !== PYTHON_LANGUAGE; + } + })(documentManager, this.configService, disposables); + interpreterDisplay.registerVisibilityFilter(filter); + disposables.push( + this.onDidChangeInterpreters((e): void => { + const interpreter = e.old ?? e.new; + if (interpreter) { + this.didChangeInterpreterInformation.fire(interpreter); + for (const { path, workspaceFolder } of this.activeInterpreterPaths.values()) { + if (path === interpreter.path && !e.new) { + // If the active environment got deleted, notify it. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path, + resource: workspaceFolder, + }); + } } - }) + } + }), + ); + disposables.push( + documentManager.onDidOpenTextDocument(() => { + // To handle scenario when language mode is set to "python" + filter.interpreterVisibilityEmitter.fire(); + }), + documentManager.onDidChangeActiveTextEditor((e): void => { + filter.interpreterVisibilityEmitter.fire(); + if (e && e.document) { + this.refresh(e.document.uri); + } + }), ); - return interpreters; + disposables.push(this.interpreterPathService.onDidChange((i) => this._onConfigChanged(i.uri))); } - public dispose(): void { - this.locator.dispose(); - this.didChangeInterpreterEmitter.dispose(); - this.didChangeInterpreterInformation.dispose(); + public getInterpreters(resource?: Uri): PythonEnvironment[] { + return this.pyenvs.getInterpreters(resource); } - 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; - } + public async getAllInterpreters(resource?: Uri): Promise { + // For backwards compatibility with old Jupyter APIs, ensure a + // fresh refresh is always triggered when using the API. As it is + // no longer auto-triggered by the extension. + this.triggerRefresh(undefined, { ifNotTriggerredAlready: true }).ignoreErrors(); + await this.refreshPromise; + return this.getInterpreters(resource); + } - return this.getInterpreterDetails(fullyQualifiedPath, resource); + public dispose(): void { + this.didChangeInterpreterEmitter.dispose(); + this.didChangeInterpreterInformation.dispose(); } - 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; + public async getActiveInterpreter(resource?: Uri): Promise { + if (useEnvExtension()) { + return getActiveInterpreterLegacy(resource); } - 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; + const activatedEnvLaunch = this.serviceContainer.get(IActivatedEnvironmentLaunch); + let path = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(true); + // This is being set as interpreter in background, after which it'll show up in `.pythonPath` config. + // However we need not wait on the update to take place, as we can use the value directly. + if (!path) { + path = this.configService.getSettings(resource).pythonPath; + if (pathUtils.basename(path) === path) { + // Value can be `python`, `python3`, `python3.9` etc. + // Note the following triggers autoselection if no interpreter is explictly + // selected, i.e the value is `python`. + // 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((ex) => { + traceError(ex); + }) + : undefined; + // Python path is invalid or python isn't installed. + if (!fullyQualifiedPath) { + return undefined; + } + path = fullyQualifiedPath; } - // 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; + return this.getInterpreterDetails(path); } - /** - * 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 getInterpreterDetails(pythonPath: string): Promise { + return this.pyenvs.getInterpreterDetails(pythonPath); } - public async getInterpreterCache( - pythonPath: string - ): Promise> { - const fileHash = (pythonPath ? await this.getInterepreterFileHash(pythonPath).catch(() => '') : '') || ''; - const store = this.persistentStateFactory.createGlobalPersistentState<{ - fileHash: string; - info?: PythonInterpreter; - }>(`${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 + + public async _onConfigChanged(resource?: Uri): Promise { + // Check if we actually changed our python path. + // Config service also updates itself on interpreter config change, + // so yielding control here to make sure it goes first and updates + // itself before we can query it. + await sleep(1); const pySettings = this.configService.getSettings(resource); + this.didChangeInterpreterConfigurationEmitter.fire(resource); if (this._pythonPathSetting === '' || this._pythonPathSetting !== pySettings.pythonPath) { this._pythonPathSetting = pySettings.pythonPath; - this.didChangeInterpreterEmitter.fire(); + this.didChangeInterpreterEmitter.fire(resource); + const workspaceFolder = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolder(resource); + reportActiveInterpreterChanged({ + path: pySettings.pythonPath, + resource: workspaceFolder, + }); + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); + await this.ensureEnvironmentContainsPython(this._pythonPathSetting, workspaceFolder); } - }; - protected async getInterepreterFileHash(pythonPath: string): Promise { - return this.hashProviderFactory - .create({ pythonPath }) - .then((provider) => provider.getInterpreterHash(pythonPath)); - } - protected async updateCachedInterpreterInformation(info: PythonInterpreter, 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.type && info.type === InterpreterType.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.type) { - const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); - const name = interpreterHelper.getInterpreterTypeDisplayName(info.type); - 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) { + @cache(-1, true) + private async ensureEnvironmentContainsPython(pythonPath: string, workspaceFolder: WorkspaceFolder | undefined) { + if (useEnvExtension()) { return; } - const details: Partial = { - ...(info as PythonInterpreter), - path: pythonPath, - type: type - }; - const envName = - type === InterpreterType.Unknown - ? undefined - : await virtualEnvManager.getEnvironmentName(pythonPath, resource); - const pthonInfo = { - ...(details as PythonInterpreter), - envName - }; - pthonInfo.displayName = await this.getDisplayName(pthonInfo, resource); - return pthonInfo; + const installer = this.serviceContainer.get(IInstaller); + if (!(await installer.isInstalled(Product.python))) { + // If Python is not installed into the environment, install it. + sendTelemetryEvent(EventName.ENVIRONMENT_WITHOUT_PYTHON_SELECTED); + const shell = this.serviceContainer.get(IApplicationShell); + const progressOptions: ProgressOptions = { + location: ProgressLocation.Window, + title: `[${Interpreters.installingPython}](command:${Commands.ViewOutput})`, + }; + traceLog('Conda envs without Python are known to not work well; fixing conda environment...'); + const promise = installer.install(Product.python, await this.getInterpreterDetails(pythonPath)); + shell.withProgress(progressOptions, () => promise); + promise + .then(async () => { + // Fetch interpreter details so the cache is updated to include the newly installed Python. + await this.getInterpreterDetails(pythonPath); + // Fire an event as the executable for the environment has changed. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path: pythonPath, + resource: workspaceFolder, + }); + }) + .ignoreErrors(); + } } } diff --git a/src/client/interpreter/interpreterVersion.ts b/src/client/interpreter/interpreterVersion.ts deleted file mode 100644 index 47d551a94122..000000000000 --- a/src/client/interpreter/interpreterVersion.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 index 6b36fba8eb94..d67d8c1d7da0 100644 --- a/src/client/interpreter/locators/types.ts +++ b/src/client/interpreter/locators/types.ts @@ -14,57 +14,3 @@ export interface IPipEnvServiceHelper { getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string } | undefined>; trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise; } - -export const IInterpreterHashProviderFactory = Symbol('IInterpreterHashProviderFactory'); -/** - * 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 interface IInterpreterHashProviderFactory { - create(options: { pythonPath: string } | { resource: Uri }): Promise; -} - -export const IInterpreterHashProvider = Symbol('IInterpreterHashProvider'); -/** - * 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 const IWindowsStoreHashProvider = Symbol('IWindowStoreHashProvider'); -export interface IWindowsStoreHashProvider extends IInterpreterHashProvider {} - -export const IWindowsStoreInterpreter = Symbol('IWindowsStoreInterpreter'); -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 index 3ba92d220e1a..f54f8e5368fe 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -5,187 +5,119 @@ 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 { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; +import { EnvironmentTypeComparer } from './configuration/environmentTypeComparer'; +import { InstallPythonCommand } from './configuration/interpreterSelector/commands/installPython'; +import { InstallPythonViaTerminal } from './configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; 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 { RecommendedEnvironmentService } from './configuration/recommededEnvironmentService'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; import { IInterpreterComparer, + IInterpreterQuickPick, IInterpreterSelector, + IRecommendedEnvironmentService, IPythonPathUpdaterServiceFactory, - IPythonPathUpdaterServiceManager + 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 { IActivatedEnvironmentLaunch, IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts'; +import { InterpreterDisplay } from './display'; +import { InterpreterLocatorProgressStatusBarHandler } from './display/progressDisplay'; import { InterpreterHelper } from './helpers'; +import { InterpreterPathCommand } from './interpreterPathCommand'; import { InterpreterService } from './interpreterService'; -import { InterpreterVersionService } from './interpreterVersion'; +import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch'; 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`. + * This method is created for testing purposes. Registers all interpreter types except `IInterpreterAutoSelectionProxyService`, `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) { + +export function registerInterpreterTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, - InterpreterSecurityStorage + InstallPythonCommand, ); serviceManager.addSingleton( IExtensionSingleActivationService, - SetInterpreterCommand + InstallPythonViaTerminal, ); serviceManager.addSingleton( IExtensionSingleActivationService, - ResetInterpreterCommand + SetInterpreterCommand, ); serviceManager.addSingleton( IExtensionSingleActivationService, - SetShebangInterpreterCommand + ResetInterpreterCommand, ); - 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( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, ); + serviceManager.addBinding(IRecommendedEnvironmentService, IExtensionActivationService); + serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); - serviceManager.addSingleton(IInterpreterVersionService, InterpreterVersionService); + serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); serviceManager.addSingleton(IInterpreterService, InterpreterService); serviceManager.addSingleton(IInterpreterDisplay, InterpreterDisplay); + serviceManager.addBinding(IInterpreterDisplay, IExtensionSingleActivationService); serviceManager.addSingleton( IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory + PythonPathUpdaterServiceFactory, ); serviceManager.addSingleton( IPythonPathUpdaterServiceManager, - PythonPathUpdaterService + PythonPathUpdaterService, ); serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); - serviceManager.addSingleton(IShebangCodeLensProvider, ShebangCodeLensProvider); serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); - serviceManager.addSingleton(IInterpreterComparer, InterpreterComparer); + serviceManager.addSingleton(IInterpreterComparer, EnvironmentTypeComparer); - serviceManager.addSingleton( - IInterpreterLocatorProgressHandler, - InterpreterLocatorProgressStatubarHandler + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterLocatorProgressStatusBarHandler, ); - 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 + InterpreterAutoSelectionService, ); serviceManager.addSingleton(IExtensionActivationService, CondaInheritEnvPrompt); - - serviceManager.addSingleton( - IExtensionSingleActivationService, - PreWarmActivatedEnvironmentVariables - ); - serviceManager.addSingleton( - IInterpreterStatusbarVisibilityFilter, - AlwaysDisplayStatusBar - ); + serviceManager.addSingleton(IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch); } -export function registerTypes(serviceManager: IServiceManager) { +export function registerTypes(serviceManager: IServiceManager): void { registerInterpreterTypes(serviceManager); - serviceManager.addSingleton( - IInterpreterAutoSeletionProxyService, - InterpreterAutoSeletionProxyService + serviceManager.addSingleton( + IInterpreterAutoSelectionProxyService, + InterpreterAutoSelectionProxyService, ); serviceManager.addSingleton( EnvironmentActivationService, - EnvironmentActivationService - ); - serviceManager.addSingleton( - TerminalEnvironmentActivationService, - TerminalEnvironmentActivationService + EnvironmentActivationService, ); serviceManager.addSingleton( IEnvironmentActivationService, - EnvironmentActivationService + EnvironmentActivationService, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterPathCommand, ); } diff --git a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts new file mode 100644 index 000000000000..6b4334e13100 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import * as path from 'path'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { sleep } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { traceError, traceLog, traceVerbose, traceWarn } from '../../logging'; +import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IPythonPathUpdaterServiceManager } from '../configuration/types'; +import { IActivatedEnvironmentLaunch, IInterpreterService } from '../contracts'; + +@injectable() +export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + private inMemorySelection: string | undefined; + + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPythonPathUpdaterServiceManager) + private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + public wasSelected: boolean = false, + ) {} + + @cache(-1, true) + public async _promptIfApplicable(): Promise { + const baseCondaPrefix = getPrefixOfActivatedCondaEnv(); + if (!baseCondaPrefix) { + return; + } + const info = await this.interpreterService.getInterpreterDetails(baseCondaPrefix); + if (info?.envName !== 'base') { + // Only show prompt for base conda environments, as we need to check config for such envs which can be slow. + return; + } + const conda = await Conda.getConda(); + if (!conda) { + traceWarn('Conda not found even though activated environment vars are set'); + return; + } + const service = await this.processServiceFactory.create(); + const autoActivateBaseConfig = await service + .shellExec(`${conda.shellCommand} config --get auto_activate_base`) + .catch((ex) => { + traceError(ex); + return { stdout: '' }; + }); + if (autoActivateBaseConfig.stdout.trim().toLowerCase().endsWith('false')) { + await this.promptAndUpdate(baseCondaPrefix); + } + } + + private async promptAndUpdate(prefix: string) { + this.wasSelected = true; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No']; + const selection = await this.appShell.showInformationMessage(Interpreters.activatedCondaEnvLaunch, ...prompts); + sendTelemetryEvent(EventName.ACTIVATED_CONDA_ENV_LAUNCH, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.setInterpeterInStorage(prefix); + } + } + + public async selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { + if (this.wasSelected) { + return this.inMemorySelection; + } + return this._selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection); + } + + @cache(-1, true) + private async _selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { + if (process.env.VSCODE_CLI !== '1') { + // We only want to select the interpreter if VS Code was launched from the command line. + traceLog("Skipping ActivatedEnv Detection: process.env.VSCODE_CLI !== '1'"); + return undefined; + } + traceVerbose('VS Code was not launched from the command line'); + const prefix = await this.getPrefixOfSelectedActivatedEnv(); + if (!prefix) { + this._promptIfApplicable().ignoreErrors(); + return undefined; + } + this.wasSelected = true; + this.inMemorySelection = prefix; + traceLog( + `VS Code was launched from an activated environment: '${path.basename( + prefix, + )}', selecting it as the interpreter for workspace.`, + ); + if (doNotBlockOnSelection) { + this.setInterpeterInStorage(prefix).ignoreErrors(); + } else { + await this.setInterpeterInStorage(prefix); + await sleep(1); // Yield control so config service can update itself. + } + this.inMemorySelection = undefined; // Once we have set the prefix in storage, clear the in memory selection. + return prefix; + } + + private async setInterpeterInStorage(prefix: string) { + const { workspaceFolders } = this.workspaceService; + if (!workspaceFolders || workspaceFolders.length === 0) { + await this.pythonPathUpdaterService.updatePythonPath(prefix, ConfigurationTarget.Global, 'load'); + } else { + await this.pythonPathUpdaterService.updatePythonPath( + prefix, + ConfigurationTarget.WorkspaceFolder, + 'load', + workspaceFolders[0].uri, + ); + } + } + + private async getPrefixOfSelectedActivatedEnv(): Promise { + const virtualEnvVar = process.env.VIRTUAL_ENV; + if (virtualEnvVar !== undefined && virtualEnvVar.length > 0) { + return virtualEnvVar; + } + const condaPrefixVar = getPrefixOfActivatedCondaEnv(); + if (!condaPrefixVar) { + return undefined; + } + const info = await this.interpreterService.getInterpreterDetails(condaPrefixVar); + if (info?.envName !== 'base') { + return condaPrefixVar; + } + // Ignoring base conda environments, as they could be automatically set by conda. + if (process.env.CONDA_AUTO_ACTIVATE_BASE !== undefined) { + if (process.env.CONDA_AUTO_ACTIVATE_BASE.toLowerCase() === 'false') { + return condaPrefixVar; + } + } + return undefined; + } +} + +function getPrefixOfActivatedCondaEnv() { + const condaPrefixVar = process.env.CONDA_PREFIX; + if (condaPrefixVar && condaPrefixVar.length > 0) { + const condaShlvl = process.env.CONDA_SHLVL; + if (condaShlvl !== undefined && condaShlvl.length > 0 && condaShlvl > '0') { + return condaPrefixVar; + } + } + return undefined; +} diff --git a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts index f3e8aa1a66ed..6b5295724449 100644 --- a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable } 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 { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../common/application/types'; import { IPlatformService } from '../../common/platform/types'; -import { IBrowserService, IPersistentStateFactory } from '../../common/types'; +import { IPersistentStateFactory } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; -import { InterpreterType } from '../../pythonEnvironments/info'; +import { traceDecoratorError, traceError } from '../../logging'; +import { EnvironmentType } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; @@ -18,21 +18,22 @@ export const condaInheritEnvPromptKey = 'CONDA_INHERIT_ENV_PROMPT_KEY'; @injectable() export class CondaInheritEnvPrompt implements IExtensionActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; 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 + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + public hasPromptBeenShownInCurrentSession: boolean = false, ) {} public async activate(resource: Uri): Promise { this.initializeInBackground(resource).ignoreErrors(); } - @traceDecorators.error('Failed to intialize conda inherit env prompt') + @traceDecoratorError('Failed to intialize conda inherit env prompt') public async initializeInBackground(resource: Uri): Promise { const show = await this.shouldShowPrompt(resource); if (!show) { @@ -41,20 +42,20 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { await this.promptAndUpdate(); } - @traceDecorators.error('Failed to display conda inherit env prompt') + @traceDecoratorError('Failed to display conda inherit env prompt') public async promptAndUpdate() { const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( condaInheritEnvPromptKey, - true + 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); + const prompts = [Common.allow, Common.close]; + const telemetrySelections: ['Allow', 'Close'] = ['Allow', 'Close']; + const selection = await this.appShell.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts); sendTelemetryEvent(EventName.CONDA_INHERIT_ENV_PROMPT, undefined, { - selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, }); if (!selection) { return; @@ -65,21 +66,24 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { .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') + @traceDecoratorError('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.appEnvironment.remoteName) { + // `terminal.integrated.inheritEnv` is only applicable user scope, so won't apply + // in remote scenarios: https://github.com/microsoft/vscode/issues/147421 + return false; + } if (this.platformService.isWindows) { return false; } const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (!interpreter || interpreter.type !== InterpreterType.Conda) { + if (!interpreter || interpreter.envType !== EnvironmentType.Conda) { return false; } const setting = this.workspaceService @@ -87,7 +91,7 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { .inspect('integrated.inheritEnv'); if (!setting) { traceError( - 'WorkspaceConfiguration.inspect returns `undefined` for setting `terminal.integrated.inheritEnv`' + 'WorkspaceConfiguration.inspect returns `undefined` for setting `terminal.integrated.inheritEnv`', ); return false; } diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts deleted file mode 100644 index a88fdd9e2324..000000000000 --- a/src/client/interpreter/virtualEnvs/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -// 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 { InterpreterType } from '../../pythonEnvironments/info'; -import { IPipEnvService } 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(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)) || InterpreterType.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)) === InterpreterType.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)) === InterpreterType.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)) === InterpreterType.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)) === InterpreterType.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 deleted file mode 100644 index 076d9ed9b736..000000000000 --- a/src/client/interpreter/virtualEnvs/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { InterpreterType } 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 index 09eafae72862..7ed18c0e8b2a 100644 --- a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts @@ -1,79 +1,75 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; +import { inject, injectable } 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 { PythonInterpreter } from '../../pythonEnvironments/info'; +import { traceDecoratorError, traceVerbose } from '../../logging'; +import { isCreatingEnvironment } from '../../pythonEnvironments/creation/createEnvApi'; +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'; +import { IComponentAdapter, IInterpreterHelper, IInterpreterService } from '../contracts'; const doNotDisplayPromptStateKey = 'MESSAGE_KEY_FOR_VIRTUAL_ENV'; @injectable() export class VirtualEnvironmentPrompt implements IExtensionActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + 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 + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, ) {} public async activate(resource: Uri): Promise { - const watcher = await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource); - watcher.onDidCreate( - () => { - this.handleNewEnvironment(resource).ignoreErrors(); - }, - this, - this.disposableRegistry - ); + const disposable = this.pyenvs.onDidCreate(resource, () => this.handleNewEnvironment(resource)); + this.disposableRegistry.push(disposable); } - @traceDecorators.error('Error in event handler for detection of new environment') + @traceDecoratorError('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 (isCreatingEnvironment()) { + return; + } + const interpreters = await this.pyenvs.getWorkspaceVirtualEnvInterpreters(resource); + const interpreter = + Array.isArray(interpreters) && interpreters.length > 0 + ? this.helper.getBestInterpreter(interpreters) + : undefined; if (!interpreter) { return; } + const currentInterpreter = await this.interpreterService.getActiveInterpreter(resource); + if (currentInterpreter?.id === interpreter.id) { + traceVerbose('New environment has already been selected'); + return; + } await this.notifyUser(interpreter, resource); } - protected async notifyUser(interpreter: PythonInterpreter, resource: Uri): Promise { + + protected async notifyUser(interpreter: PythonEnvironment, resource: Uri): Promise { const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState( doNotDisplayPromptStateKey, - true + true, ); if (!notificationPromptEnabled.value) { return; } - const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; + 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 - ); + const selection = await this.appShell.showInformationMessage(Interpreters.environmentPromptMessage, ...prompts); sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT, undefined, { - selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, }); if (!selection) { return; @@ -83,7 +79,7 @@ export class VirtualEnvironmentPrompt implements IExtensionActivationService { interpreter.path, ConfigurationTarget.WorkspaceFolder, 'ui', - resource + resource, ); } else if (selection === prompts[2]) { await notificationPromptEnabled.updateValue(false); diff --git a/src/client/ioc/container.ts b/src/client/ioc/container.ts index 07db6f95bca2..0f1302061a67 100644 --- a/src/client/ioc/container.ts +++ b/src/client/ioc/container.ts @@ -3,7 +3,7 @@ import { EventEmitter } from 'events'; import { Container, decorate, injectable, interfaces } from 'inversify'; -import { traceWarning } from '../common/logger'; +import { traceWarn } from '../logging'; import { Abstract, IServiceContainer, Newable } from './types'; // This needs to be done once, hence placed in a common location. @@ -13,26 +13,29 @@ import { Abstract, IServiceContainer, Newable } from './types'; try { decorate(injectable(), EventEmitter); } catch (ex) { - traceWarning('Failed to decorate EventEmitter for DI (possibly already decorated by another Extension)', ex); + traceWarn('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 + 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 + name?: string | number | symbol | undefined, ): T | undefined { try { return name @@ -41,5 +44,7 @@ export class ServiceContainer implements IServiceContainer { } catch { // This might happen after the container has been destroyed } + + return undefined; } } diff --git a/src/client/ioc/index.ts b/src/client/ioc/index.ts deleted file mode 100644 index 0ee4070d5ded..000000000000 --- a/src/client/ioc/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 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 index 837308dff514..a575b25e8c3f 100644 --- a/src/client/ioc/serviceManager.ts +++ b/src/client/ioc/serviceManager.ts @@ -9,12 +9,14 @@ 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 + + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor: new (...args: any[]) => T, name?: string | number | symbol | undefined, - bindings?: symbol[] + bindings?: symbol[], ): void { if (name) { this.container.bind(serviceIdentifier).to(constructor).whenTargetNamed(name); @@ -28,9 +30,10 @@ export class ServiceManager implements IServiceManager { }); } } + public addFactory( factoryIdentifier: interfaces.ServiceIdentifier>, - factoryMethod: interfaces.FactoryCreator + factoryMethod: interfaces.FactoryCreator, ): void { this.container.bind>(factoryIdentifier).toFactory(factoryMethod); } @@ -41,10 +44,11 @@ export class ServiceManager implements IServiceManager { public addSingleton( serviceIdentifier: identifier, - // tslint:disable-next-line:no-any + + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor: new (...args: any[]) => T, name?: string | number | symbol | undefined, - bindings?: symbol[] + bindings?: symbol[], ): void { if (name) { this.container.bind(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); @@ -62,7 +66,7 @@ export class ServiceManager implements IServiceManager { public addSingletonInstance( serviceIdentifier: identifier, instance: T, - name?: string | number | symbol | undefined + name?: string | number | symbol | undefined, ): void { if (name) { this.container.bind(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); @@ -70,9 +74,11 @@ export class ServiceManager implements IServiceManager { 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 @@ -81,7 +87,10 @@ export class ServiceManager implements IServiceManager { } catch { // This might happen after the container has been destroyed } + + return undefined; } + public getAll(serviceIdentifier: identifier, name?: string | number | symbol | undefined): T[] { return name ? this.container.getAllNamed(serviceIdentifier, name) @@ -91,7 +100,7 @@ export class ServiceManager implements IServiceManager { public rebind( serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, - name?: string | number | symbol + name?: string | number | symbol, ): void { if (name) { this.container.rebind(serviceIdentifier).to(constructor).whenTargetNamed(name); @@ -103,7 +112,7 @@ export class ServiceManager implements IServiceManager { public rebindSingleton( serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, - name?: string | number | symbol + name?: string | number | symbol, ): void { if (name) { this.container.rebind(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); @@ -115,7 +124,7 @@ export class ServiceManager implements IServiceManager { public rebindInstance( serviceIdentifier: interfaces.ServiceIdentifier, instance: T, - name?: string | number | symbol + name?: string | number | symbol, ): void { if (name) { this.container.rebind(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); @@ -124,7 +133,7 @@ export class ServiceManager implements IServiceManager { } } - public dispose() { + public dispose(): void { this.container.unbindAll(); this.container.unload(); } diff --git a/src/client/ioc/types.ts b/src/client/ioc/types.ts index 079fca02e5bd..0a18e44824ed 100644 --- a/src/client/ioc/types.ts +++ b/src/client/ioc/types.ts @@ -4,25 +4,19 @@ 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 + // eslint-disable-next-line @typescript-eslint/no-explicit-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 + // eslint-disable-next-line @typescript-eslint/no-explicit-any new (...args: any[]): T; }; -//tslint:enable:callable-types export const IServiceManager = Symbol('IServiceManager'); @@ -31,22 +25,22 @@ export interface IServiceManager extends IDisposable { serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol | undefined, - bindings?: symbol[] + bindings?: symbol[], ): void; addSingleton( serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol, - bindings?: symbol[] + bindings?: symbol[], ): void; addSingletonInstance( serviceIdentifier: interfaces.ServiceIdentifier, instance: T, - name?: string | number | symbol + name?: string | number | symbol, ): void; addFactory( factoryIdentifier: interfaces.ServiceIdentifier>, - factoryMethod: interfaces.FactoryCreator + factoryMethod: interfaces.FactoryCreator, ): void; addBinding(from: interfaces.ServiceIdentifier, to: interfaces.ServiceIdentifier): void; get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T; @@ -55,17 +49,17 @@ export interface IServiceManager extends IDisposable { rebind( serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, - name?: string | number | symbol + name?: string | number | symbol, ): void; rebindSingleton( serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, - name?: string | number | symbol + name?: string | number | symbol, ): void; rebindInstance( serviceIdentifier: interfaces.ServiceIdentifier, instance: T, - name?: string | number | symbol + name?: string | number | symbol, ): void; } diff --git a/src/client/jupyter/jupyterExtensionDependencyManager.ts b/src/client/jupyter/jupyterExtensionDependencyManager.ts new file mode 100644 index 000000000000..defd5ea38241 --- /dev/null +++ b/src/client/jupyter/jupyterExtensionDependencyManager.ts @@ -0,0 +1,13 @@ +import { inject, injectable } from 'inversify'; +import { IJupyterExtensionDependencyManager } from '../common/application/types'; +import { JUPYTER_EXTENSION_ID } from '../common/constants'; +import { IExtensions } from '../common/types'; + +@injectable() +export class JupyterExtensionDependencyManager implements IJupyterExtensionDependencyManager { + constructor(@inject(IExtensions) private extensions: IExtensions) {} + + public get isJupyterExtensionInstalled(): boolean { + return this.extensions.getExtension(JUPYTER_EXTENSION_ID) !== undefined; + } +} diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts new file mode 100644 index 000000000000..5584682f3b86 --- /dev/null +++ b/src/client/jupyter/jupyterIntegration.ts @@ -0,0 +1,306 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak, max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { dirname } from 'path'; +import { EventEmitter, Extension, Memento, Uri, workspace, Event } from 'vscode'; +import type { SemVer } from 'semver'; +import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; +import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { GLOBAL_MEMENTO, IExtensions, IMemento, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { + IInterpreterQuickPickItem, + IInterpreterSelector, + IRecommendedEnvironmentService, +} from '../interpreter/configuration/types'; +import { + ICondaService, + IInterpreterDisplay, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter, +} from '../interpreter/contracts'; +import { PylanceApi } from '../activation/node/pylanceApi'; +import { ExtensionContextKey } from '../common/application/contextKeys'; +import { getDebugpyPath } from '../debugger/pythonDebugger'; +import type { Environment, EnvironmentPath, PythonExtension } from '../api/types'; +import { DisposableBase } from '../common/utils/resourceLifecycle'; + +type PythonApiForJupyterExtension = { + /** + * IEnvironmentActivationService + */ + getActivatedEnvironmentVariables( + resource: Resource, + interpreter: Environment, + allowExceptions?: boolean, + ): Promise; + getKnownSuggestions(resource: Resource): IInterpreterQuickPickItem[]; + /** + * @deprecated Use `getKnownSuggestions` and `suggestionToQuickPickItem` instead. + */ + getSuggestions(resource: Resource): Promise; + /** + * Returns path to where `debugpy` is. In python extension this is `/python_files/lib/python`. + */ + getDebuggerPath(): Promise; + /** + * Retrieve interpreter path selected for Jupyter server from Python memento storage + */ + getInterpreterPathSelectedForJupyterServer(): string | undefined; + /** + * Registers a visibility filter for the interpreter status bar. + */ + registerInterpreterStatusFilter(filter: IInterpreterStatusbarVisibilityFilter): void; + getCondaVersion(): Promise; + /** + * Returns the conda executable. + */ + getCondaFile(): Promise; + + /** + * Call to provide a function that the Python extension can call to request the Python + * path to use for a particular notebook. + * @param func : The function that Python should call when requesting the Python path. + */ + registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; + + /** + * Returns the preferred environment for the given URI. + */ + getRecommededEnvironment( + uri: Uri | undefined, + ): Promise< + | { + environment: EnvironmentPath; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; +}; + +type JupyterExtensionApi = { + /** + * Registers python extension specific parts with the jupyter extension + * @param interpreterService + */ + registerPythonApi(interpreterService: PythonApiForJupyterExtension): void; +}; + +@injectable() +export class JupyterExtensionIntegration { + private jupyterExtension: Extension | undefined; + + private pylanceExtension: Extension | undefined; + private environmentApi: PythonExtension['environments'] | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + @inject(IInterpreterDisplay) private interpreterDisplay: IInterpreterDisplay, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IRecommendedEnvironmentService) private preferredEnvironmentService: IRecommendedEnvironmentService, + ) {} + public registerEnvApi(api: PythonExtension['environments']) { + this.environmentApi = api; + } + + public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { + this.contextManager.setContext(ExtensionContextKey.IsJupyterInstalled, true); + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(jupyterExtensionApi)); + return undefined; + } + // Forward python parts + jupyterExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async ( + resource: Resource, + env: Environment, + allowExceptions?: boolean, + ) => { + const interpreter = await this.interpreterService.getInterpreterDetails(env.path); + return this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions); + }, + getSuggestions: async (resource: Resource): Promise => + this.interpreterSelector.getAllSuggestions(resource), + getKnownSuggestions: (resource: Resource): IInterpreterQuickPickItem[] => + this.interpreterSelector.getSuggestions(resource), + getDebuggerPath: async () => dirname(await getDebugpyPath()), + getInterpreterPathSelectedForJupyterServer: () => + this.globalState.get('INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'), + registerInterpreterStatusFilter: this.interpreterDisplay.registerVisibilityFilter.bind( + this.interpreterDisplay, + ), + getCondaFile: () => this.condaService.getCondaFile(), + getCondaVersion: () => this.condaService.getCondaVersion(), + registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => + this.registerJupyterPythonPathFunction(func), + getRecommededEnvironment: async (uri) => { + if (!this.environmentApi) { + return undefined; + } + return this.preferredEnvironmentService.getRecommededEnvironment(uri); + }, + }); + return undefined; + } + + public async integrateWithJupyterExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.pylanceExtension) { + const pylanceExtension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + + if (pylanceExtension && !pylanceExtension.isActive) { + await pylanceExtension.activate(); + } + + this.pylanceExtension = pylanceExtension; + } + + if (!this.jupyterExtension) { + const jupyterExtension = this.extensions.getExtension(JUPYTER_EXTENSION_ID); + if (!jupyterExtension) { + return undefined; + } + await jupyterExtension.activate(); + if (jupyterExtension.isActive) { + this.jupyterExtension = jupyterExtension; + return this.jupyterExtension.exports; + } + } else { + return this.jupyterExtension.exports; + } + return undefined; + } + + private getPylanceApi(): PylanceApi | undefined { + const api = this.pylanceExtension?.exports; + return api && api.notebook && api.client && api.client.isEnabled() ? api : undefined; + } + + private registerJupyterPythonPathFunction(func: (uri: Uri) => Promise) { + const api = this.getPylanceApi(); + if (api) { + api.notebook!.registerJupyterPythonPathFunction(func); + } + } +} + +export interface JupyterPythonEnvironmentApi { + /** + * This event is triggered when the environment associated with a Jupyter Notebook or Interactive Window changes. + * The Uri in the event is the Uri of the Notebook/IW. + */ + onDidChangePythonEnvironment?: Event; + /** + * Returns the EnvironmentPath to the Python environment associated with a Jupyter Notebook or Interactive Window. + * If the Uri is not associated with a Jupyter Notebook or Interactive Window, then this method returns undefined. + * @param uri + */ + getPythonEnvironment?( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + }; +} + +@injectable() +export class JupyterExtensionPythonEnvironments extends DisposableBase implements JupyterPythonEnvironmentApi { + private jupyterExtension?: JupyterPythonEnvironmentApi; + + private readonly _onDidChangePythonEnvironment = this._register(new EventEmitter()); + + public readonly onDidChangePythonEnvironment = this._onDidChangePythonEnvironment.event; + + constructor(@inject(IExtensions) private readonly extensions: IExtensions) { + super(); + } + + public getPythonEnvironment( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + } { + if (!isJupyterResource(uri)) { + return undefined; + } + const api = this.getJupyterApi(); + if (api?.getPythonEnvironment) { + return api.getPythonEnvironment(uri); + } + return undefined; + } + + private getJupyterApi() { + if (!this.jupyterExtension) { + const ext = this.extensions.getExtension(JUPYTER_EXTENSION_ID); + if (!ext) { + return undefined; + } + if (!ext.isActive) { + ext.activate().then(() => { + this.hookupOnDidChangePythonEnvironment(ext.exports); + }); + return undefined; + } + this.hookupOnDidChangePythonEnvironment(ext.exports); + } + return this.jupyterExtension; + } + + private hookupOnDidChangePythonEnvironment(api: JupyterPythonEnvironmentApi) { + this.jupyterExtension = api; + if (api.onDidChangePythonEnvironment) { + this._register( + api.onDidChangePythonEnvironment( + this._onDidChangePythonEnvironment.fire, + this._onDidChangePythonEnvironment, + ), + ); + } + } +} + +function isJupyterResource(resource: Uri): boolean { + // Jupyter extension only deals with Notebooks and Interactive Windows. + return ( + resource.fsPath.endsWith('.ipynb') || + workspace.notebookDocuments.some((item) => item.uri.toString() === resource.toString()) + ); +} diff --git a/src/client/jupyter/requireJupyterPrompt.ts b/src/client/jupyter/requireJupyterPrompt.ts new file mode 100644 index 000000000000..3e6878ba4269 --- /dev/null +++ b/src/client/jupyter/requireJupyterPrompt.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { Common, Interpreters } from '../common/utils/localize'; +import { Commands, JUPYTER_EXTENSION_ID } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +@injectable() +export class RequireJupyterPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallJupyter, () => this._showPrompt())); + } + + public async _showPrompt(): Promise { + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No']; + const selection = await this.appShell.showInformationMessage(Interpreters.requireJupyter, ...prompts); + sendTelemetryEvent(EventName.REQUIRE_JUPYTER_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.commandManager.executeCommand( + 'workbench.extensions.installExtension', + JUPYTER_EXTENSION_ID, + undefined, + ); + } + } +} diff --git a/src/client/jupyter/types.ts b/src/client/jupyter/types.ts new file mode 100644 index 000000000000..5eb58c7cf2b2 --- /dev/null +++ b/src/client/jupyter/types.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { QuickPickItem } from 'vscode'; + +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +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; +} + +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; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; diff --git a/src/client/language/braceCounter.ts b/src/client/language/braceCounter.ts deleted file mode 100644 index 30d91b537544..000000000000 --- a/src/client/language/braceCounter.ts +++ /dev/null @@ -1,71 +0,0 @@ -// 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 deleted file mode 100644 index 09f3bed33f9d..000000000000 --- a/src/client/language/characterStream.ts +++ /dev/null @@ -1,134 +0,0 @@ -// 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 deleted file mode 100644 index 50d5f716a039..000000000000 --- a/src/client/language/characters.ts +++ /dev/null @@ -1,104 +0,0 @@ -// 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 deleted file mode 100644 index c69c62132e73..000000000000 --- a/src/client/language/iterableTextRange.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 index 9e80d3a3b704..0fbcd29c645a 100644 --- a/src/client/language/languageConfiguration.ts +++ b/src/client/language/languageConfiguration.ts @@ -1,13 +1,11 @@ // 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: [ @@ -21,8 +19,8 @@ export function getLanguageConfiguration(): LanguageConfiguration { $ `), action: { - indentAction: IndentAction.Indent - } + indentAction: IndentAction.Indent, + }, }, // continue comments { @@ -30,8 +28,8 @@ export function getLanguageConfiguration(): LanguageConfiguration { afterText: /.+$/, action: { indentAction: IndentAction.None, - appendText: '# ' - } + appendText: '# ', + }, }, // indent on enter (block-beginning statements) { @@ -59,7 +57,9 @@ export function getLanguageConfiguration(): LanguageConfiguration { elif | while | with | - async \\s+ with + async \\s+ with | + match | + case ) \\b .* ) | @@ -74,19 +74,25 @@ export function getLanguageConfiguration(): LanguageConfiguration { $ `), action: { - indentAction: IndentAction.Indent - } + indentAction: IndentAction.Indent, + }, }, // outdent on enter (block-ending statements) { + /** + * This does not handle all cases. Notable omissions here are + * "return" and "raise" which are complicated by the need to + * only outdent when the cursor is at the end of an expression + * rather than, say, between the parentheses of a tail-call or + * exception construction. (see issue #10583) + */ beforeText: verboseRegExp(` ^ (?: (?: \\s* (?: - pass | - raise \\s+ [^#\\s] [^#]* + pass ) ) | (?: @@ -103,12 +109,12 @@ export function getLanguageConfiguration(): LanguageConfiguration { $ `), action: { - indentAction: IndentAction.Outdent - } - } + 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 deleted file mode 100644 index e11f2a1299c4..000000000000 --- a/src/client/language/textBuilder.ts +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 6037cbf67cb9..000000000000 --- a/src/client/language/textIterator.ts +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index 8ce5a744c9a6..000000000000 --- a/src/client/language/textRangeCollection.ts +++ /dev/null @@ -1,105 +0,0 @@ -// 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 deleted file mode 100644 index f51ceaa350c7..000000000000 --- a/src/client/language/tokenizer.ts +++ /dev/null @@ -1,514 +0,0 @@ -// 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 deleted file mode 100644 index 51618039a3d4..000000000000 --- a/src/client/language/types.ts +++ /dev/null @@ -1,105 +0,0 @@ -// 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 deleted file mode 100644 index 9b3ca0b15b25..000000000000 --- a/src/client/language/unicode.ts +++ /dev/null @@ -1,64 +0,0 @@ -// 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/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts new file mode 100644 index 000000000000..4cbfb6f33466 --- /dev/null +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { JediLanguageServerAnalysisOptions } from '../activation/jedi/analysisOptions'; +import { JediLanguageClientFactory } from '../activation/jedi/languageClientFactory'; +import { JediLanguageServerProxy } from '../activation/jedi/languageServerProxy'; +import { JediLanguageServerManager } from '../activation/jedi/manager'; +import { ILanguageServerOutputChannel } from '../activation/types'; +import { IWorkspaceService, ICommandManager } from '../common/application/types'; +import { + IExperimentService, + IInterpreterPathService, + IConfigurationService, + Resource, + IDisposable, +} from '../common/types'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceError } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ILanguageServerExtensionManager } from './types'; + +export class JediLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { + private serverProxy: JediLanguageServerProxy; + + serverManager: JediLanguageServerManager; + + clientFactory: JediLanguageClientFactory; + + analysisOptions: JediLanguageServerAnalysisOptions; + + constructor( + serviceContainer: IServiceContainer, + outputChannel: ILanguageServerOutputChannel, + _experimentService: IExperimentService, + workspaceService: IWorkspaceService, + configurationService: IConfigurationService, + _interpreterPathService: IInterpreterPathService, + interpreterService: IInterpreterService, + environmentService: IEnvironmentVariablesProvider, + commandManager: ICommandManager, + ) { + this.analysisOptions = new JediLanguageServerAnalysisOptions( + environmentService, + outputChannel, + configurationService, + workspaceService, + ); + this.clientFactory = new JediLanguageClientFactory(interpreterService); + this.serverProxy = new JediLanguageServerProxy(this.clientFactory); + this.serverManager = new JediLanguageServerManager( + serviceContainer, + this.analysisOptions, + this.serverProxy, + commandManager, + ); + } + + dispose(): void { + this.serverManager.disconnect(); + this.serverManager.dispose(); + this.serverProxy.dispose(); + this.analysisOptions.dispose(); + } + + async startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + async stopLanguageServer(): Promise { + this.serverManager.disconnect(); + await this.serverProxy.stop(); + } + + // eslint-disable-next-line class-methods-use-this + canStartLanguageServer(interpreter: PythonEnvironment | undefined): boolean { + if (!interpreter) { + traceError('Unable to start Jedi language server as a valid interpreter is not selected'); + return false; + } + // Otherwise return true for now since it's shipped with the extension. + // Update this when JediLSP is pulled in a separate extension. + return true; + } + + // eslint-disable-next-line class-methods-use-this + languageServerNotAvailable(): Promise { + // Nothing to do here. + // Update this when JediLSP is pulled in a separate extension. + return Promise.resolve(); + } +} diff --git a/src/client/languageServer/noneLSExtensionManager.ts b/src/client/languageServer/noneLSExtensionManager.ts new file mode 100644 index 000000000000..1d93ea50be51 --- /dev/null +++ b/src/client/languageServer/noneLSExtensionManager.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable class-methods-use-this */ + +import { ILanguageServerExtensionManager } from './types'; + +// This LS manager implements ILanguageServer directly +// instead of extending LanguageServerCapabilities because it doesn't need to do anything. +export class NoneLSExtensionManager implements ILanguageServerExtensionManager { + dispose(): void { + // Nothing to do here. + } + + startLanguageServer(): Promise { + return Promise.resolve(); + } + + stopLanguageServer(): Promise { + return Promise.resolve(); + } + + canStartLanguageServer(): boolean { + return true; + } + + languageServerNotAvailable(): Promise { + // Nothing to do here. + return Promise.resolve(); + } +} diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts new file mode 100644 index 000000000000..7b03d909a512 --- /dev/null +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { promptForPylanceInstall } from '../activation/common/languageServerChangeHandler'; +import { NodeLanguageServerAnalysisOptions } from '../activation/node/analysisOptions'; +import { NodeLanguageClientFactory } from '../activation/node/languageClientFactory'; +import { NodeLanguageServerProxy } from '../activation/node/languageServerProxy'; +import { NodeLanguageServerManager } from '../activation/node/manager'; +import { ILanguageServerOutputChannel } from '../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../common/constants'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposable, + IExperimentService, + IExtensions, + IInterpreterPathService, + Resource, +} from '../common/types'; +import { Pylance } from '../common/utils/localize'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceLog } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ILanguageServerExtensionManager } from './types'; + +export class PylanceLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { + private serverProxy: NodeLanguageServerProxy; + + serverManager: NodeLanguageServerManager; + + clientFactory: NodeLanguageClientFactory; + + analysisOptions: NodeLanguageServerAnalysisOptions; + + constructor( + serviceContainer: IServiceContainer, + outputChannel: ILanguageServerOutputChannel, + experimentService: IExperimentService, + readonly workspaceService: IWorkspaceService, + readonly configurationService: IConfigurationService, + interpreterPathService: IInterpreterPathService, + _interpreterService: IInterpreterService, + environmentService: IEnvironmentVariablesProvider, + readonly commandManager: ICommandManager, + fileSystem: IFileSystem, + private readonly extensions: IExtensions, + readonly applicationShell: IApplicationShell, + ) { + this.analysisOptions = new NodeLanguageServerAnalysisOptions(outputChannel, workspaceService); + this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); + this.serverProxy = new NodeLanguageServerProxy( + this.clientFactory, + experimentService, + interpreterPathService, + environmentService, + workspaceService, + extensions, + ); + this.serverManager = new NodeLanguageServerManager( + serviceContainer, + this.analysisOptions, + this.serverProxy, + commandManager, + extensions, + ); + } + + dispose(): void { + this.serverManager.disconnect(); + this.serverManager.dispose(); + this.serverProxy.dispose(); + this.analysisOptions.dispose(); + } + + async startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + async stopLanguageServer(): Promise { + this.serverManager.disconnect(); + await this.serverProxy.stop(); + } + + canStartLanguageServer(): boolean { + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + return !!extension; + } + + async languageServerNotAvailable(): Promise { + await promptForPylanceInstall( + this.applicationShell, + this.commandManager, + this.workspaceService, + this.configurationService, + ); + + traceLog(Pylance.pylanceNotInstalledMessage); + } +} diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts new file mode 100644 index 000000000000..f7cad157fcef --- /dev/null +++ b/src/client/languageServer/types.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { LanguageServerType } from '../activation/types'; +import { Resource } from '../common/types'; +import { PythonEnvironment } from '../pythonEnvironments/info'; + +export const ILanguageServerWatcher = Symbol('ILanguageServerWatcher'); +/** + * The language server watcher serves as a singleton that watches for changes to the language server setting, + * and instantiates the relevant language server extension manager. + */ +export interface ILanguageServerWatcher { + readonly languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + readonly languageServerType: LanguageServerType; + startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise; + restartLanguageServers(): Promise; + get(resource: Resource, interpreter?: PythonEnvironment): Promise; +} + +/** + * `ILanguageServerExtensionManager` implementations act as wrappers for anything related to their specific language server extension. + * They are responsible for starting and stopping the language server provided by their LS extension. + * They also extend the `ILanguageServer` interface via `ILanguageServerCapabilities` to continue supporting the Jupyter integration. + */ +export interface ILanguageServerExtensionManager { + startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise; + stopLanguageServer(): Promise; + canStartLanguageServer(interpreter: PythonEnvironment | undefined): boolean; + languageServerNotAvailable(): Promise; + dispose(): void; +} diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts new file mode 100644 index 000000000000..39e6e0bb1ece --- /dev/null +++ b/src/client/languageServer/watcher.ts @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, l10n, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; +import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; +import { IExtensionActivationService, ILanguageServerOutputChannel, LanguageServerType } from '../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExtensions, + IInterpreterPathService, + InterpreterConfigurationScope, + Resource, +} from '../common/types'; +import { LanguageService } from '../common/utils/localize'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterHelper, IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceLog } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { JediLSExtensionManager } from './jediLSExtensionManager'; +import { NoneLSExtensionManager } from './noneLSExtensionManager'; +import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; +import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { StopWatch } from '../common/utils/stopWatch'; + +@injectable() +/** + * The Language Server Watcher class implements the ILanguageServerWatcher interface, which is the one-stop shop for language server activation. + */ +export class LanguageServerWatcher implements IExtensionActivationService, ILanguageServerWatcher { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + + languageServerType: LanguageServerType; + + private workspaceInterpreters: Map; + + // In a multiroot workspace scenario we may have multiple language servers running: + // When using Jedi, there will be one language server per workspace folder. + // When using Pylance, there will only be one language server for the project. + private workspaceLanguageServers: Map; + + private registered = false; + + constructor( + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IEnvironmentVariablesProvider) private readonly environmentService: IEnvironmentVariablesProvider, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IFileSystem) private readonly fileSystem: IFileSystem, + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IApplicationShell) readonly applicationShell: IApplicationShell, + @inject(IDisposableRegistry) readonly disposables: IDisposableRegistry, + ) { + this.workspaceInterpreters = new Map(); + this.workspaceLanguageServers = new Map(); + this.languageServerType = this.configurationService.getSettings().languageServer; + } + + // IExtensionActivationService + + public async activate(resource?: Resource, startupStopWatch?: StopWatch): Promise { + this.register(); + await this.startLanguageServer(this.languageServerType, resource, startupStopWatch); + } + + // ILanguageServerWatcher + public async startLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + startupStopWatch?: StopWatch, + ): Promise { + await this.startAndGetLanguageServer(languageServerType, resource, startupStopWatch); + } + + public register(): void { + if (!this.registered) { + this.registered = true; + this.disposables.push( + this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)), + ); + + this.disposables.push( + this.workspaceService.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders.bind(this)), + ); + + this.disposables.push( + this.interpreterService.onDidChangeInterpreterInformation(this.onDidChangeInterpreterInformation, this), + ); + + if (this.workspaceService.isTrusted) { + this.disposables.push(this.interpreterPathService.onDidChange(this.onDidChangeInterpreter.bind(this))); + } + + this.disposables.push( + this.extensions.onDidChange(async () => { + await this.extensionsChangeHandler(); + }), + ); + + this.disposables.push( + new LanguageServerChangeHandler( + this.languageServerType, + this.extensions, + this.applicationShell, + this.commandManager, + this.workspaceService, + this.configurationService, + ), + ); + } + } + + private async startAndGetLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + startupStopWatch?: StopWatch, + ): Promise { + const lsResource = this.getWorkspaceUri(resource); + const currentInterpreter = this.workspaceInterpreters.get(lsResource.fsPath); + const interpreter = await this.interpreterService?.getActiveInterpreter(resource); + + // Destroy the old language server if it's different. + if (currentInterpreter && interpreter !== currentInterpreter) { + await this.stopLanguageServer(lsResource); + } + + // If the interpreter is Python 2 and the LS setting is explicitly set to Jedi, turn it off. + // If set to Default, use Pylance. + let serverType = languageServerType; + if (interpreter && (interpreter.version?.major ?? 0) < 3) { + if (serverType === LanguageServerType.Jedi) { + serverType = LanguageServerType.None; + } else if (this.getCurrentLanguageServerTypeIsDefault()) { + serverType = LanguageServerType.Node; + } + } + + if ( + !this.workspaceService.isTrusted && + serverType !== LanguageServerType.Node && + serverType !== LanguageServerType.None + ) { + traceLog(LanguageService.untrustedWorkspaceMessage); + serverType = LanguageServerType.None; + } + + // If the language server type is Pylance or None, + // We only need to instantiate the language server once, even in multiroot workspace scenarios, + // so we only need one language server extension manager. + const key = this.getWorkspaceKey(resource, serverType); + const languageServer = this.workspaceLanguageServers.get(key); + if ((serverType === LanguageServerType.Node || serverType === LanguageServerType.None) && languageServer) { + logStartup(serverType, lsResource); + return languageServer; + } + + // Instantiate the language server extension manager. + const languageServerExtensionManager = this.createLanguageServer(serverType); + this.workspaceLanguageServers.set(key, languageServerExtensionManager); + + if (languageServerExtensionManager.canStartLanguageServer(interpreter)) { + // Start the language server. + if (startupStopWatch) { + // It means that startup is triggering this code, track time it takes since startup to activate this code. + sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRIGGER_TIME, startupStopWatch.elapsedTime, { + triggerTime: startupStopWatch.elapsedTime, + }); + } + await languageServerExtensionManager.startLanguageServer(lsResource, interpreter); + + logStartup(languageServerType, lsResource); + this.languageServerType = languageServerType; + this.workspaceInterpreters.set(lsResource.fsPath, interpreter); + } else { + await languageServerExtensionManager.languageServerNotAvailable(); + } + + return languageServerExtensionManager; + } + + public async restartLanguageServers(): Promise { + this.workspaceLanguageServers.forEach(async (_, resourceString) => { + sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'notebooksExperiment' }); + const resource = Uri.parse(resourceString); + await this.stopLanguageServer(resource); + await this.startLanguageServer(this.languageServerType, resource); + }); + } + + public async get(resource?: Resource): Promise { + const key = this.getWorkspaceKey(resource, this.languageServerType); + let languageServerExtensionManager = this.workspaceLanguageServers.get(key); + + if (!languageServerExtensionManager) { + languageServerExtensionManager = await this.startAndGetLanguageServer(this.languageServerType, resource); + } + + return Promise.resolve(languageServerExtensionManager); + } + + // Private methods + + private async stopLanguageServer(resource?: Resource): Promise { + const key = this.getWorkspaceKey(resource, this.languageServerType); + const languageServerExtensionManager = this.workspaceLanguageServers.get(key); + + if (languageServerExtensionManager) { + await languageServerExtensionManager.stopLanguageServer(); + languageServerExtensionManager.dispose(); + this.workspaceLanguageServers.delete(key); + } + } + + private createLanguageServer(languageServerType: LanguageServerType): ILanguageServerExtensionManager { + let lsManager: ILanguageServerExtensionManager; + switch (languageServerType) { + case LanguageServerType.Jedi: + lsManager = new JediLSExtensionManager( + this.serviceContainer, + this.lsOutputChannel, + this.experimentService, + this.workspaceService, + this.configurationService, + this.interpreterPathService, + this.interpreterService, + this.environmentService, + this.commandManager, + ); + break; + case LanguageServerType.Node: + lsManager = new PylanceLSExtensionManager( + this.serviceContainer, + this.lsOutputChannel, + this.experimentService, + this.workspaceService, + this.configurationService, + this.interpreterPathService, + this.interpreterService, + this.environmentService, + this.commandManager, + this.fileSystem, + this.extensions, + this.applicationShell, + ); + break; + case LanguageServerType.None: + default: + lsManager = new NoneLSExtensionManager(); + break; + } + + this.disposables.push({ + dispose: async () => { + await lsManager.stopLanguageServer(); + lsManager.dispose(); + }, + }); + return lsManager; + } + + private async refreshLanguageServer(resource?: Resource, forced?: boolean): Promise { + const lsResource = this.getWorkspaceUri(resource); + const languageServerType = this.configurationService.getSettings(lsResource).languageServer; + + if (languageServerType !== this.languageServerType || forced) { + await this.stopLanguageServer(resource); + await this.startLanguageServer(languageServerType, lsResource); + } + } + + private getCurrentLanguageServerTypeIsDefault(): boolean { + return this.configurationService.getSettings().languageServerIsDefault; + } + + // Watch for settings changes. + private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { + const workspacesUris = this.workspaceService.workspaceFolders?.map((workspace) => workspace.uri) ?? []; + + workspacesUris.forEach(async (resource) => { + if (event.affectsConfiguration(`python.languageServer`, resource)) { + await this.refreshLanguageServer(resource); + } else if (event.affectsConfiguration(`python.analysis.pylanceLspClientEnabled`, resource)) { + await this.refreshLanguageServer(resource, /* forced */ true); + } + }); + } + + // Watch for interpreter changes. + private async onDidChangeInterpreter(event: InterpreterConfigurationScope): Promise { + if (this.languageServerType === LanguageServerType.Node) { + // Pylance client already handles interpreter changes, so restarting LS can be skipped. + return Promise.resolve(); + } + // Reactivate the language server (if in a multiroot workspace scenario, pick the correct one). + return this.activate(event.uri); + } + + // Watch for interpreter information changes. + private async onDidChangeInterpreterInformation(info: PythonEnvironment): Promise { + if (!info.envPath || info.envPath === '') { + return; + } + + // Find the interpreter and workspace that got updated (if any). + const iterator = this.workspaceInterpreters.entries(); + + let result = iterator.next(); + let done = result.done || false; + + while (!done) { + const [resourcePath, interpreter] = result.value as [string, PythonEnvironment | undefined]; + const resource = Uri.parse(resourcePath); + + // Restart the language server if the interpreter path changed (#18995). + if (info.envPath === interpreter?.envPath && info.path !== interpreter?.path) { + await this.activate(resource); + done = true; + } else { + result = iterator.next(); + done = result.done || false; + } + } + } + + // Watch for extension changes. + private async extensionsChangeHandler(): Promise { + const languageServerType = this.configurationService.getSettings().languageServer; + + if (languageServerType !== this.languageServerType) { + await this.refreshLanguageServer(); + } + } + + // Watch for workspace folder changes. + private async onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): Promise { + // Since Jedi is the only language server type where we instantiate multiple language servers, + // Make sure to dispose of them only in that scenario. + if (event.removed.length && this.languageServerType === LanguageServerType.Jedi) { + for (const workspace of event.removed) { + await this.stopLanguageServer(workspace.uri); + } + } + } + + // Get the workspace Uri for the given resource, in order to query this.workspaceInterpreters and this.workspaceLanguageServers. + private getWorkspaceUri(resource?: Resource): Uri { + let uri; + + if (resource) { + uri = this.workspaceService.getWorkspaceFolder(resource)?.uri; + } else { + uri = this.interpreterHelper.getActiveWorkspaceUri(resource)?.folderUri; + } + + return uri ?? Uri.parse('default'); + } + + // Get the key used to identify which language server extension manager is associated to which workspace. + // When using Pylance or having no LS enabled, we return a static key since there should only be one LS extension manager for these LS types. + private getWorkspaceKey(resource: Resource | undefined, languageServerType: LanguageServerType): string { + switch (languageServerType) { + case LanguageServerType.Node: + return 'Pylance'; + case LanguageServerType.None: + return 'None'; + default: + return this.getWorkspaceUri(resource).fsPath; + } + } +} + +function logStartup(languageServerType: LanguageServerType, resource: Uri): void { + let outputLine; + const basename = path.basename(resource.fsPath); + + switch (languageServerType) { + case LanguageServerType.Jedi: + outputLine = l10n.t('Starting Jedi language server for {0}.', basename); + break; + case LanguageServerType.Node: + outputLine = LanguageService.startingPylance; + break; + case LanguageServerType.None: + outputLine = LanguageService.startingNone; + break; + default: + throw new Error(`Unknown language server type: ${languageServerType}`); + } + traceLog(outputLine); +} diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts deleted file mode 100644 index d151d563d650..000000000000 --- a/src/client/languageServices/jediProxyFactory.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Disposable, Uri, workspace } from 'vscode'; - -import { IServiceContainer } from '../ioc/types'; -import { ICommandResult, JediProxy, JediProxyHandler } from '../providers/jediProxy'; -import { PythonInterpreter } from '../pythonEnvironments/info'; - -export class JediFactory implements Disposable { - private disposables: Disposable[]; - private jediProxyHandlers: Map>; - - constructor( - private interpreter: PythonInterpreter | 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 deleted file mode 100644 index 13acc4527005..000000000000 --- a/src/client/languageServices/proposeLanguageServerBanner.ts +++ /dev/null @@ -1,100 +0,0 @@ -// 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'; - -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() - ); - - if (response === Pylance.tryItNow()) { - this.appShell.openUrl(getPylanceExtensionUri(this.appEnv)); - await this.disable(); - } else if (response === Common.bannerLabelNo()) { - await this.disable(); - } else { - this.disabledInCurrentSession = true; - } - } - - 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 deleted file mode 100644 index 137d95d48275..000000000000 --- a/src/client/linters/bandit.ts +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 3799e31660b3..000000000000 --- a/src/client/linters/baseLinter.ts +++ /dev/null @@ -1,210 +0,0 @@ -// 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 { 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'; - -// 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; -} - -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 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 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; - } - - 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); - } - - public get info(): ILinterInfo { - return this._info; - } - - public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { - this._pythonSettings = this.configService.getSettings(document.uri); - return this.runLinter(document, cancellation); - } - - 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; - - // 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; - } - - 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; - } - } - } 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 deleted file mode 100644 index 3f1bfa3f5dcc..000000000000 --- a/src/client/linters/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -// 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 deleted file mode 100644 index 7ef13f4e615c..000000000000 --- a/src/client/linters/errorHandlers/baseErrorHandler.ts +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index ea4009ddd85d..000000000000 --- a/src/client/linters/errorHandlers/errorHandler.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 16871e7ee71f..000000000000 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 088206269c8f..000000000000 --- a/src/client/linters/errorHandlers/standard.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 5d2cd0d4e6c1..000000000000 --- a/src/client/linters/flake8.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 Flake8 extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.flake8, 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.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 deleted file mode 100644 index 834a86284610..000000000000 --- a/src/client/linters/linterAvailability.ts +++ /dev/null @@ -1,144 +0,0 @@ -// 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 deleted file mode 100644 index 3f7f11dcc206..000000000000 --- a/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,108 +0,0 @@ -// 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 deleted file mode 100644 index 307afe4c0cc2..000000000000 --- a/src/client/linters/linterInfo.ts +++ /dev/null @@ -1,113 +0,0 @@ -// 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 deleted file mode 100644 index a9dd8e1c9c70..000000000000 --- a/src/client/linters/linterManager.ts +++ /dev/null @@ -1,161 +0,0 @@ -// 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 deleted file mode 100644 index c9bf05ff8e74..000000000000 --- a/src/client/linters/lintingEngine.ts +++ /dev/null @@ -1,194 +0,0 @@ -// 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 deleted file mode 100644 index eff5c71be37a..000000000000 --- a/src/client/linters/mypy.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/prospector.ts b/src/client/linters/prospector.ts deleted file mode 100644 index 2341b49e155b..000000000000 --- a/src/client/linters/prospector.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as path from 'path'; -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 Prospector extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.prospector, outputChannel, serviceContainer); - } - - 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 []; - } - 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 deleted file mode 100644 index 2d425ab29b18..000000000000 --- a/src/client/linters/pycodestyle.ts +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 718e8b359b21..000000000000 --- a/src/client/linters/pydocstyle.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as path from 'path'; -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 PyDocStyle extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.pydocstyle, outputChannel, serviceContainer); - } - - 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; - }); - - return messages; - } - - 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. - 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]); - } - - 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 { - if (line.trim().length === 0) { - return; - } - 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(); - - const sourceLine = document.lineAt(lineNumber - 1).text; - const trmmedSourceLine = sourceLine.trim(); - const sourceStart = sourceLine.indexOf(trmmedSourceLine); - - // tslint:disable-next-line:no-object-literal-type-assertion - return { - code: code, - message: message, - column: sourceStart, - line: lineNumber, - type: '', - provider: this.info.id - } as ILintMessage; - } catch (ex) { - traceError(`Failed to parse pydocstyle line '${line}'`, ex); - return; - } - }) - .filter((item) => item !== undefined) - .map((item) => item!) - ); - } -} diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts deleted file mode 100644 index c1159b2d9544..000000000000 --- a/src/client/linters/pylama.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index cf3474427dd7..000000000000 --- a/src/client/linters/pylint.ts +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as os from 'os'; -import * as path from 'path'; -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 pylintrc = 'pylintrc'; -const dotPylintrc = '.pylintrc'; - -const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?[\\w-]+):(?.*)\\r?(\\n|$)'; - -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); - } - - 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 messages; - } - - // 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; - } - - 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)); - - 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; - } -} diff --git a/src/client/linters/serviceRegistry.ts b/src/client/linters/serviceRegistry.ts deleted file mode 100644 index 89ea2da1c10e..000000000000 --- a/src/client/linters/serviceRegistry.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index 6dba6e9d15ac..000000000000 --- a/src/client/linters/types.ts +++ /dev/null @@ -1,87 +0,0 @@ -// 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 deleted file mode 100644 index dabd864551f3..000000000000 --- a/src/client/logging/_global.ts +++ /dev/null @@ -1,117 +0,0 @@ -// 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/fileLogger.ts b/src/client/logging/fileLogger.ts new file mode 100644 index 000000000000..47e77f18d802 --- /dev/null +++ b/src/client/logging/fileLogger.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { WriteStream } from 'fs-extra'; +import * as util from 'util'; +import { Disposable } from 'vscode-jsonrpc'; +import { Arguments, ILogging } from './types'; +import { getTimeForLogging } from './util'; + +function formatMessage(level?: string, ...data: Arguments): string { + return level + ? `[${level.toUpperCase()} ${getTimeForLogging()}]: ${util.format(...data)}\r\n` + : `${util.format(...data)}\r\n`; +} + +export class FileLogger implements ILogging, Disposable { + constructor(private readonly stream: WriteStream) {} + + public traceLog(...data: Arguments): void { + this.stream.write(formatMessage(undefined, ...data)); + } + + public traceError(...data: Arguments): void { + this.stream.write(formatMessage('error', ...data)); + } + + public traceWarn(...data: Arguments): void { + this.stream.write(formatMessage('warn', ...data)); + } + + public traceInfo(...data: Arguments): void { + this.stream.write(formatMessage('info', ...data)); + } + + public traceVerbose(...data: Arguments): void { + this.stream.write(formatMessage('debug', ...data)); + } + + public dispose(): void { + try { + this.stream.close(); + } catch (ex) { + /** do nothing */ + } + } +} diff --git a/src/client/logging/formatters.ts b/src/client/logging/formatters.ts deleted file mode 100644 index b3dd4e52761f..000000000000 --- a/src/client/logging/formatters.ts +++ /dev/null @@ -1,81 +0,0 @@ -// 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 index 861c98f38a41..39d5652e100a 100644 --- a/src/client/logging/index.ts +++ b/src/client/logging/index.ts @@ -1,16 +1,215 @@ // 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'; + +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { createWriteStream } from 'fs-extra'; +import { isPromise } from 'rxjs/internal-compatibility'; +import { Disposable } from 'vscode'; +import { StopWatch } from '../common/utils/stopWatch'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { FileLogger } from './fileLogger'; +import { Arguments, ILogging, LogLevel, TraceDecoratorType, TraceOptions } from './types'; +import { argsToLogString, returnValueToLogString } from './util'; + +const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; + +let loggers: ILogging[] = []; +export function registerLogger(logger: ILogging): Disposable { + loggers.push(logger); + return { + dispose: () => { + loggers = loggers.filter((l) => l !== logger); + }, + }; +} + +export function initializeFileLogging(disposables: Disposable[]): void { + if (process.env.VSC_PYTHON_LOG_FILE) { + const fileLogger = new FileLogger(createWriteStream(process.env.VSC_PYTHON_LOG_FILE)); + disposables.push(fileLogger); + disposables.push(registerLogger(fileLogger)); + } +} + +export function traceLog(...args: Arguments): void { + loggers.forEach((l) => l.traceLog(...args)); +} + +export function traceError(...args: Arguments): void { + loggers.forEach((l) => l.traceError(...args)); +} + +export function traceWarn(...args: Arguments): void { + loggers.forEach((l) => l.traceWarn(...args)); +} + +export function traceInfo(...args: Arguments): void { + loggers.forEach((l) => l.traceInfo(...args)); +} + +export function traceVerbose(...args: Arguments): void { + loggers.forEach((l) => l.traceVerbose(...args)); +} + +/** Logging Decorators go here */ + +export function traceDecoratorVerbose(message: string, opts: TraceOptions = DEFAULT_OPTS): TraceDecoratorType { + return createTracingDecorator({ message, opts, level: LogLevel.Debug }); +} +export function traceDecoratorError(message: string): TraceDecoratorType { + return createTracingDecorator({ message, opts: DEFAULT_OPTS, level: LogLevel.Error }); +} +export function traceDecoratorInfo(message: string): TraceDecoratorType { + return createTracingDecorator({ message, opts: DEFAULT_OPTS, level: LogLevel.Info }); +} +export function traceDecoratorWarn(message: string): TraceDecoratorType { + return createTracingDecorator({ message, opts: DEFAULT_OPTS, level: LogLevel.Warning }); +} + +// Information about a function/method call. +type CallInfo = { + kind: string; // "Class", etc. + name: string; + + args: unknown[]; +}; + +// Information about a traced function/method call. +type TraceInfo = { + elapsed: number; // milliseconds + // Either returnValue or err will be set. + + returnValue?: any; + err?: Error; +}; + +type LogInfo = { + opts: TraceOptions; + message: string; + level?: LogLevel; +}; + +// Return a decorator that traces the decorated function. +function traceDecorator(log: (c: CallInfo, t: TraceInfo) => void): TraceDecoratorType { + return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function (...args: unknown[]) { + const call = { + kind: 'Class', + name: _ && _.constructor ? _.constructor.name : '', + args, + }; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const scope = this; + return tracing( + // "log()" + (t) => log(call, t), + // "run()" + () => originalMethod.apply(scope, args), + ); + }; + + return descriptor; + }; +} + +// Call run(), call log() with the trace info, and return the result. +function tracing(log: (t: TraceInfo) => void, run: () => T): T { + const timer = new StopWatch(); + try { + const result = run(); + + // If method being wrapped returns a promise then wait for it. + if (isPromise(result)) { + ((result as unknown) as Promise) + .then((data) => { + log({ elapsed: timer.elapsedTime, returnValue: data }); + return data; + }) + .catch((ex) => { + log({ elapsed: timer.elapsedTime, err: ex }); + + // 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 as Error | undefined }); + throw ex; + } +} + +function createTracingDecorator(logInfo: LogInfo): TraceDecoratorType { + return traceDecorator((call, traced) => logResult(logInfo, traced, call)); +} + +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(logInfo: LogInfo, traced: TraceInfo, call?: CallInfo): string { + call = normalizeCall(call!); + const messages = [logInfo.message]; + messages.push( + `${call.kind} name = ${call.name}`.trim(), + `completed in ${traced.elapsed}ms`, + `has a ${traced.returnValue ? 'truthy' : 'falsy'} return value`, + ); + if ((logInfo.opts & TraceOptions.Arguments) === TraceOptions.Arguments) { + messages.push(argsToLogString(call.args)); + } + if ((logInfo.opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { + messages.push(returnValueToLogString(traced.returnValue)); + } + return messages.join(', '); +} + +function logResult(logInfo: LogInfo, traced: TraceInfo, call?: CallInfo) { + const formatted = formatMessages(logInfo, traced, call); + if (traced.err === undefined) { + // The call did not fail. + if (!logInfo.level || logInfo.level > LogLevel.Error) { + logTo(LogLevel.Info, [formatted]); + } + } else { + logTo(LogLevel.Error, [formatted, traced.err]); + sendTelemetryEvent(('ERROR' as unknown) as EventName, undefined, undefined, traced.err); + } +} + +export function logTo(logLevel: LogLevel, ...args: Arguments): void { + switch (logLevel) { + case LogLevel.Error: + traceError(...args); + break; + case LogLevel.Warning: + traceWarn(...args); + break; + case LogLevel.Info: + traceInfo(...args); + break; + case LogLevel.Debug: + traceVerbose(...args); + break; + default: + break; + } +} diff --git a/src/client/logging/levels.ts b/src/client/logging/levels.ts deleted file mode 100644 index c0982dd3c282..000000000000 --- a/src/client/logging/levels.ts +++ /dev/null @@ -1,103 +0,0 @@ -// 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 deleted file mode 100644 index ab5ba55cb519..000000000000 --- a/src/client/logging/logger.ts +++ /dev/null @@ -1,127 +0,0 @@ -// 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/outputChannelLogger.ts b/src/client/logging/outputChannelLogger.ts new file mode 100644 index 000000000000..40505d33a735 --- /dev/null +++ b/src/client/logging/outputChannelLogger.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as util from 'util'; +import { LogOutputChannel } from 'vscode'; +import { Arguments, ILogging } from './types'; + +export class OutputChannelLogger implements ILogging { + constructor(private readonly channel: LogOutputChannel) {} + + public traceLog(...data: Arguments): void { + this.channel.appendLine(util.format(...data)); + } + + public traceError(...data: Arguments): void { + this.channel.error(util.format(...data)); + } + + public traceWarn(...data: Arguments): void { + this.channel.warn(util.format(...data)); + } + + public traceInfo(...data: Arguments): void { + this.channel.info(util.format(...data)); + } + + public traceVerbose(...data: Arguments): void { + this.channel.debug(util.format(...data)); + } +} diff --git a/src/client/logging/trace.ts b/src/client/logging/trace.ts deleted file mode 100644 index 3191df5bd9d7..000000000000 --- a/src/client/logging/trace.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index 2a8e83a5fa52..000000000000 --- a/src/client/logging/transports.ts +++ /dev/null @@ -1,114 +0,0 @@ -// 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/types.ts b/src/client/logging/types.ts new file mode 100644 index 000000000000..c05800868512 --- /dev/null +++ b/src/client/logging/types.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type Arguments = unknown[]; + +export enum LogLevel { + Off = 0, + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, +} + +export interface ILogging { + traceLog(...data: Arguments): void; + traceError(...data: Arguments): void; + traceWarn(...data: Arguments): void; + traceInfo(...data: Arguments): void; + traceVerbose(...data: Arguments): void; +} + +export type TraceDecoratorType = ( + _: Object, + __: string, + descriptor: TypedPropertyDescriptor, +) => TypedPropertyDescriptor; + +// The information we want to log. +export enum TraceOptions { + None = 0, + Arguments = 1, + ReturnValue = 2, +} diff --git a/src/client/logging/util.ts b/src/client/logging/util.ts index 79d977136a89..4229fd7976a5 100644 --- a/src/client/logging/util.ts +++ b/src/client/logging/util.ts @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -// tslint:disable-next-line:no-any -export type Arguments = any[]; +import { Uri } from 'vscode'; + +export type Arguments = unknown[]; function valueToLogString(value: unknown, kind: string): string { if (value === undefined) { @@ -13,10 +15,8 @@ function valueToLogString(value: unknown, kind: string): string { return 'null'; } try { - // tslint:disable-next-line:no-any - if (value && (value as any).fsPath) { - // tslint:disable-next-line:no-any - return ``; + if (value && (value as Uri).fsPath) { + return ``; } return JSON.stringify(value); } catch { @@ -47,3 +47,8 @@ export function returnValueToLogString(returnValue: unknown): string { const valueString = valueToLogString(returnValue, 'Return value'); return `Return Value: ${valueString}`; } + +export function getTimeForLogging(): string { + const date = new Date(); + return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}`; +} diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts new file mode 100644 index 000000000000..22d53b0201ef --- /dev/null +++ b/src/client/proposedApi.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceContainer } from './ioc/types'; +import { ProposedExtensionAPI } from './proposedApiTypes'; +import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { buildDeprecatedProposedApi } from './deprecatedProposedApi'; +import { DeprecatedProposedAPI } from './deprecatedProposedApiTypes'; + +export function buildProposedApi( + discoveryApi: IDiscoveryAPI, + serviceContainer: IServiceContainer, +): ProposedExtensionAPI { + /** + * @deprecated Will be removed soon. + */ + let deprecatedProposedApi; + try { + deprecatedProposedApi = { ...buildDeprecatedProposedApi(discoveryApi, serviceContainer) }; + } catch (ex) { + deprecatedProposedApi = {} as DeprecatedProposedAPI; + // Errors out only in case of testing. + // Also, these APIs no longer supported, no need to log error. + } + + const proposed: ProposedExtensionAPI & DeprecatedProposedAPI = { + ...deprecatedProposedApi, + }; + return proposed; +} diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts new file mode 100644 index 000000000000..13ad5af543ec --- /dev/null +++ b/src/client/proposedApiTypes.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface ProposedExtensionAPI { + /** + * Top level proposed APIs should go here. + */ +} diff --git a/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts index 805614794bd7..e90e6ea97fd2 100644 --- a/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts +++ b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts @@ -9,7 +9,7 @@ import { Diagnostic, Range, TextDocument, - WorkspaceEdit + WorkspaceEdit, } from 'vscode'; /** @@ -22,6 +22,7 @@ export class LaunchJsonCodeActionProvider implements CodeActionProvider { .map((diagnostic) => this.createFix(document, diagnostic)); } + // eslint-disable-next-line class-methods-use-this private createFix(document: TextDocument, diagnostic: Diagnostic): CodeAction { const finalText = `"${document.getText(diagnostic.range)}"`; const fix = new CodeAction(`Convert to ${finalText}`, CodeActionKind.QuickFix); diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts index 375c9986d949..259f42848606 100644 --- a/src/client/providers/codeActionProvider/main.ts +++ b/src/client/providers/codeActionProvider/main.ts @@ -9,19 +9,22 @@ import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; @injectable() export class CodeActionProviderService implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} + public async activate(): Promise { - // tslint:disable-next-line:no-require-imports + // eslint-disable-next-line global-require const vscode = require('vscode') as typeof vscodeTypes; const documentSelector: vscodeTypes.DocumentFilter = { scheme: 'file', language: 'jsonc', - pattern: '**/launch.json' + pattern: '**/launch.json', }; this.disposableRegistry.push( vscode.languages.registerCodeActionsProvider(documentSelector, new LaunchJsonCodeActionProvider(), { - providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] - }) + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], + }), ); } } diff --git a/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts b/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts deleted file mode 100644 index d1c45dd8610e..000000000000 --- a/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 8426cff4ae6d..000000000000 --- a/src/client/providers/completionProvider.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -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 completionSource: CompletionSource; - private configService: IConfigurationService; - - constructor(jediFactory: JediFactory, serviceContainer: IServiceContainer) { - this.completionSource = new CompletionSource(jediFactory, serviceContainer, new ItemInfoSource(jediFactory)); - this.configService = serviceContainer.get(IConfigurationService); - } - - @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); - } - } - return items; - } - - 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 deleted file mode 100644 index e135ba18be7b..000000000000 --- a/src/client/providers/completionSource.ts +++ /dev/null @@ -1,135 +0,0 @@ -// 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 deleted file mode 100644 index 9b3d8f1a7ce6..000000000000 --- a/src/client/providers/definitionProvider.ts +++ /dev/null @@ -1,59 +0,0 @@ -'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'; - -export class PythonDefinitionProvider implements vscode.DefinitionProvider { - 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); - } - } - @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 deleted file mode 100644 index 424b9b6a74ec..000000000000 --- a/src/client/providers/docStringFoldingProvider.ts +++ /dev/null @@ -1,124 +0,0 @@ -// 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/formatProvider.ts b/src/client/providers/formatProvider.ts deleted file mode 100644 index cd4d8f3956c0..000000000000 --- a/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -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[] = []; - - // 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 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; - } - - 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 deleted file mode 100644 index 13a7ed7e205f..000000000000 --- a/src/client/providers/hoverProvider.ts +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -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 itemInfoSource: ItemInfoSource; - - constructor(jediFactory: JediFactory) { - this.itemInfoSource = new ItemInfoSource(jediFactory); - } - - @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 deleted file mode 100644 index 1cdb8f46a53a..000000000000 --- a/src/client/providers/importSortProvider.ts +++ /dev/null @@ -1,204 +0,0 @@ -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 } from '../common/types'; -import { createDeferred, createDeferredFromPromise, Deferred } from '../common/utils/async'; -import { noop } from '../common/utils/misc'; -import { IServiceContainer } from '../ioc/types'; -import { captureTelemetry } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { ISortImportsEditingProvider } from './types'; - -@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 documentManager: IDocumentManager; - private readonly configurationService: IConfigurationService; - private readonly editorUtils: IEditorUtils; - - 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); - } - - @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)); - } - - 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()); - - 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); - } - } - - 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, - throwOnStdErr: true, - 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 = ''; - const isortOutput = createDeferred(); - observableResult.out.subscribe({ - next: (output) => { - if (output.source === 'stdout') { - outputBuffer += 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; - - 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 deleted file mode 100644 index b7a38a65a884..000000000000 --- a/src/client/providers/itemInfoSource.ts +++ /dev/null @@ -1,201 +0,0 @@ -// 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 deleted file mode 100644 index 85e719a96796..000000000000 --- a/src/client/providers/jediProxy.ts +++ /dev/null @@ -1,900 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-var-requires no-require-imports no-any -import { ChildProcess } from 'child_process'; -import * as path from 'path'; -// @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 { PythonInterpreter } 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)) { - const value = pythonVSCodeTypeMappings.get(pythonType); - if (value) { - return value; - } - } - return CompletionItemKind.Keyword; -} - -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 -} - -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'); - -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: PythonInterpreter | 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(); - } - - private static getProperty(o: object, name: string): T { - return (o as any)[name]; - } - - 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(); - } - - public getNextCommandId(): number { - const result = this.cmdId; - this.cmdId += 1; - return result; - } - - 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')); - } - - 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; - } - - // 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); - }); - } - private shouldCheckJediMemoryFootprint() { - if (this.ignoreJediMemoryFootprint || this.pythonSettings.jediMemoryLimit === -1) { - return false; - } - 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; - } - - 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(); - } - } - - deferred.resolve(); - }); - - return deferred.promise; - } - - // @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(); - } - - 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); - } - 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); - } - return; - } - - responses.forEach((response) => { - if (!response) { - return; - } - const responseId = JediProxy.getProperty(response, 'id'); - if (!this.commands.has(responseId)) { - return; - } - 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(); - }); - } - }, - (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); - } - - 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); - } - } - }); - } - } - - // tslint:disable-next-line:no-any - private createPayload(cmd: IExecutionCommand): any { - const payload = { - 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; - } - - 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 ''; - } - const fs = this.serviceContainer.get(IFileSystem); - const exists = await fs.fileExists(lines[0]); - return exists ? lines[0] : ''; - } catch { - return ''; - } - } - 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(() => '') - ]; - - 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() { - // 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); - } - - 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 - }; - } - - private safeResolve( - command: IExecutionCommand | undefined | null, - result: ICommandResult | PromiseLike | undefined - ): void { - if (command && command.deferred) { - command.deferred.resolve(result); - } - } -} - -// tslint:disable-next-line:no-unused-variable -export interface ICommand { - telemetryEvent?: string; - command: CommandType; - source?: string; - fileName: string; - lineIndex: number; - columnIndex: number; -} - -interface IExecutionCommand extends ICommand { - id: number; - deferred?: Deferred; - token: CancellationToken; - delay?: number; -} - -export interface ICommandError { - message: string; -} - -export interface ICommandResult { - requestId: number; -} -export interface ICompletionResult extends ICommandResult { - items: IAutoCompleteItem[]; -} -export interface IHoverResult extends ICommandResult { - items: IHoverItem[]; -} -export interface IDefinitionResult extends ICommandResult { - definitions: IDefinition[]; -} -export interface IReferenceResult extends ICommandResult { - references: IReference[]; -} -export interface ISymbolResult extends ICommandResult { - 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; -} - -export interface IAutoCompleteItem { - 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 { - rawType: string; - type: CompletionItemKind; - kind: SymbolKind; - text: string; - fileName: string; - 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; - - public get JediProxy(): JediProxy { - return this.jediProxy; - } - - public constructor(private jediProxy: JediProxy) { - this.commandCancellationTokenSources = new Map(); - } - - public dispose() { - if (this.jediProxy) { - this.jediProxy.dispose(); - } - } - - public sendCommand(cmd: ICommand, _token?: CancellationToken): Promise { - const executionCmd = >cmd; - executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); - - if (this.commandCancellationTokenSources.has(cmd.command)) { - const ct = this.commandCancellationTokenSources.get(cmd.command); - if (ct) { - ct.cancel(); - } - } - - const cancellation = new CancellationTokenSource(); - this.commandCancellationTokenSources.set(cmd.command, cancellation); - executionCmd.token = cancellation.token; - - return this.jediProxy.sendCommand(executionCmd).catch((reason) => { - traceError(reason); - return undefined; - }); - } - - 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/linterProvider.ts b/src/client/providers/linterProvider.ts deleted file mode 100644 index 8bfde47e192b..000000000000 --- a/src/client/providers/linterProvider.ts +++ /dev/null @@ -1,113 +0,0 @@ -// 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 deleted file mode 100644 index fa886cfe05ef..000000000000 --- a/src/client/providers/objectDefinitionProvider.ts +++ /dev/null @@ -1,87 +0,0 @@ -'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 deleted file mode 100644 index 7a330b51eec0..000000000000 --- a/src/client/providers/providerUtilities.ts +++ /dev/null @@ -1,32 +0,0 @@ -// 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 deleted file mode 100644 index 1a392dcb79d3..000000000000 --- a/src/client/providers/referenceProvider.ts +++ /dev/null @@ -1,77 +0,0 @@ -'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'; - -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 references; - } - return []; - } - - @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; - } - - 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(); - } - - 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 deleted file mode 100644 index 86037f894c60..000000000000 --- a/src/client/providers/renameProvider.ts +++ /dev/null @@ -1,93 +0,0 @@ -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'; - -type RenameResponse = { - results: [{ diff: string }]; -}; - -export class PythonRenameProvider implements RenameProvider { - private readonly outputChannel: OutputChannel; - constructor(private serviceContainer: IServiceContainer) { - this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - } - @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: TextDocument, position: Position, newName: string): ProviderResult { - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return; - } - if (position.character <= 0) { - return; - } - - const range = document.getWordRangeAtPosition(position); - if (!range || range.isEmpty) { - return; - } - const oldName = document.getText(range); - if (oldName === newName) { - return; - } - - 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; - - 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 index 5753df8d1d83..dd9df89a78a3 100644 --- a/src/client/providers/replProvider.ts +++ b/src/client/providers/replProvider.ts @@ -1,30 +1,43 @@ import { Disposable } from 'vscode'; import { IActiveResourceService, ICommandManager } from '../common/application/types'; import { Commands } from '../common/constants'; +import { noop } from '../common/utils/misc'; +import { IInterpreterService } from '../interpreter/contracts'; 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() { + + public dispose(): void { 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'); + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + this.serviceContainer + .get(ICommandManager) + .executeCommand(Commands.TriggerEnvironmentSelection, resource) + .then(noop, noop); + return; + } + const replProvider = this.serviceContainer.get(ICodeExecutionService, 'standard'); await replProvider.initializeRepl(resource); } } diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 66640455f08a..a96ec14ff5e9 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -6,13 +6,10 @@ 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); +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, - CodeActionProviderService + CodeActionProviderService, ); } diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts deleted file mode 100644 index df10f120b21a..000000000000 --- a/src/client/providers/signatureProvider.ts +++ /dev/null @@ -1,139 +0,0 @@ -'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 deleted file mode 100644 index 185c22522539..000000000000 --- a/src/client/providers/simpleRefactorProvider.ts +++ /dev/null @@ -1,230 +0,0 @@ -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 deleted file mode 100644 index 07327b97f3a4..000000000000 --- a/src/client/providers/symbolProvider.ts +++ /dev/null @@ -1,202 +0,0 @@ -'use strict'; - -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 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); - - 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 flattened; -} - -/** - * 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 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([]); - } - - const deferred = createDeferred(); - const timer: NodeJS.Timer | number = setTimeout(() => { - if (token.isCancellationRequested) { - return deferred.resolve([]); - } - - 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(); - } - - 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 index 15f1241d88ce..f68f151110ec 100644 --- a/src/client/providers/terminalProvider.ts +++ b/src/client/providers/terminalProvider.ts @@ -4,27 +4,37 @@ import { Disposable, Terminal } from 'vscode'; import { IActiveResourceService, ICommandManager } from '../common/application/types'; import { Commands } from '../common/constants'; +import { inTerminalEnvVarExperiment } from '../common/experiments/helpers'; import { ITerminalActivator, ITerminalServiceFactory } from '../common/terminal/types'; -import { IConfigurationService } from '../common/types'; +import { IConfigurationService, IExperimentService } from '../common/types'; import { swallowExceptions } from '../common/utils/decorators'; import { IServiceContainer } from '../ioc/types'; import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { useEnvExtension, shouldEnvExtHandleActivation } from '../envExt/api.internal'; 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) { + public async initialize(currentTerminal: Terminal | undefined): Promise { const configuration = this.serviceContainer.get(IConfigurationService); + const experimentService = this.serviceContainer.get(IExperimentService); const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); - if (currentTerminal && pythonSettings.terminal.activateEnvInCurrentTerminal) { + if ( + currentTerminal && + pythonSettings.terminal.activateEnvInCurrentTerminal && + !inTerminalEnvVarExperiment(experimentService) && + !shouldEnvExtHandleActivation() + ) { const hideFromUser = 'hideFromUser' in currentTerminal.creationOptions && currentTerminal.creationOptions.hideFromUser; if (!hideFromUser) { @@ -32,23 +42,31 @@ export class TerminalProvider implements Disposable { await terminalActivator.activateEnvironmentInTerminal(currentTerminal, { preserveFocus: true }); } sendTelemetryEvent(EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL, undefined, { - isTerminalVisible: !hideFromUser + isTerminalVisible: !hideFromUser, }); } } - public dispose() { + + public dispose(): void { 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(); + if (useEnvExtension()) { + const commandManager = this.serviceContainer.get(ICommandManager); + await commandManager.executeCommand('python-envs.createTerminal', activeResource); + } + + const terminalService = this.serviceContainer.get(ITerminalServiceFactory); await terminalService.createTerminalService(activeResource, 'Python').show(false); } } diff --git a/src/client/providers/types.ts b/src/client/providers/types.ts deleted file mode 100644 index f2d1bc6eea3a..000000000000 --- a/src/client/providers/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -// 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/pylanceApi.ts b/src/client/pylanceApi.ts new file mode 100644 index 000000000000..b839d0d9c2b7 --- /dev/null +++ b/src/client/pylanceApi.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TelemetryEventMeasurements, TelemetryEventProperties } from '@vscode/extension-telemetry'; +import { BaseLanguageClient } from 'vscode-languageclient'; + +export interface TelemetryReporter { + sendTelemetryEvent( + eventName: string, + properties?: TelemetryEventProperties, + measurements?: TelemetryEventMeasurements, + ): void; + sendTelemetryErrorEvent( + eventName: string, + properties?: TelemetryEventProperties, + measurements?: TelemetryEventMeasurements, + ): void; +} + +export interface ApiForPylance { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createClient(...args: any[]): BaseLanguageClient; + start(client: BaseLanguageClient): Promise; + stop(client: BaseLanguageClient): Promise; + getTelemetryReporter(): TelemetryReporter; +} diff --git a/src/client/pythonEnvironments/api.ts b/src/client/pythonEnvironments/api.ts new file mode 100644 index 000000000000..a2065c30b740 --- /dev/null +++ b/src/client/pythonEnvironments/api.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event } from 'vscode'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from './base/locator'; + +export type GetLocatorFunc = () => Promise; + +/** + * The public API for the Python environments component. + * + * Note that this is composed of sub-components. + */ +class PythonEnvironments implements IDiscoveryAPI { + private locator!: IDiscoveryAPI; + + constructor( + // These are factories for the sub-components the full component is composed of: + private readonly getLocator: GetLocatorFunc, + ) {} + + public async activate(): Promise { + this.locator = await this.getLocator(); + } + + public get onProgress(): Event { + return this.locator.onProgress; + } + + public get refreshState(): ProgressReportStage { + return this.locator.refreshState; + } + + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) { + return this.locator.getRefreshPromise(options); + } + + public get onChanged() { + return this.locator.onChanged; + } + + public getEnvs(query?: PythonLocatorQuery) { + return this.locator.getEnvs(query); + } + + public async resolveEnv(env: string) { + return this.locator.resolveEnv(env); + } + + public async triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions) { + return this.locator.triggerRefresh(query, options); + } +} + +export async function createPythonEnvironments(getLocator: GetLocatorFunc): Promise { + const api = new PythonEnvironments(getLocator); + await api.activate(); + return api; +} diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts new file mode 100644 index 000000000000..5c5b9317e169 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -0,0 +1,365 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep, isEqual } from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { getArchitectureDisplayName } from '../../../common/platform/registry'; +import { Architecture } from '../../../common/utils/platform'; +import { arePathsSame, isParentPath, normCasePath } from '../../common/externalDependencies'; +import { getKindDisplayName } from './envKind'; +import { areIdenticalVersion, areSimilarVersions, getVersionDisplayString, isVersionEmpty } from './pythonVersion'; + +import { + EnvPathType, + globallyInstalledEnvKinds, + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonEnvType, + PythonReleaseLevel, + PythonVersion, + virtualEnvKinds, +} from '.'; +import { BasicEnvInfo } from '../locator'; + +/** + * Create a new info object with all values empty. + * + * @param init - if provided, these values are applied to the new object + */ +export function buildEnvInfo(init?: { + kind?: PythonEnvKind; + executable?: string; + name?: string; + location?: string; + version?: PythonVersion; + org?: string; + arch?: Architecture; + fileInfo?: { ctime: number; mtime: number }; + source?: PythonEnvSource[]; + display?: string; + sysPrefix?: string; + searchLocation?: Uri; + type?: PythonEnvType; + /** + * Command used to run Python in this environment. + * E.g. `conda run -n envName python` or `python.exe` + */ + pythonRunCommand?: string[]; + identifiedUsingNativeLocator?: boolean; +}): PythonEnvInfo { + const env: PythonEnvInfo = { + name: init?.name ?? '', + location: '', + kind: PythonEnvKind.Unknown, + executable: { + filename: '', + sysPrefix: init?.sysPrefix ?? '', + ctime: init?.fileInfo?.ctime ?? -1, + mtime: init?.fileInfo?.mtime ?? -1, + }, + searchLocation: undefined, + display: init?.display, + version: { + major: -1, + minor: -1, + micro: -1, + release: { + level: PythonReleaseLevel.Final, + serial: 0, + }, + }, + arch: init?.arch ?? Architecture.Unknown, + distro: { + org: init?.org ?? '', + }, + source: init?.source ?? [], + pythonRunCommand: init?.pythonRunCommand, + identifiedUsingNativeLocator: init?.identifiedUsingNativeLocator, + }; + if (init !== undefined) { + updateEnv(env, init); + } + env.id = getEnvID(env.executable.filename, env.location); + return env; +} + +export function areEnvsDeepEqual(env1: PythonEnvInfo, env2: PythonEnvInfo): boolean { + const env1Clone = cloneDeep(env1); + const env2Clone = cloneDeep(env2); + // Cannot compare searchLocation as they are Uri objects. + delete env1Clone.searchLocation; + delete env2Clone.searchLocation; + env1Clone.source = env1Clone.source.sort(); + env2Clone.source = env2Clone.source.sort(); + const searchLocation1 = env1.searchLocation?.fsPath ?? ''; + const searchLocation2 = env2.searchLocation?.fsPath ?? ''; + const searchLocation1Scheme = env1.searchLocation?.scheme ?? ''; + const searchLocation2Scheme = env2.searchLocation?.scheme ?? ''; + return ( + isEqual(env1Clone, env2Clone) && + arePathsSame(searchLocation1, searchLocation2) && + searchLocation1Scheme === searchLocation2Scheme + ); +} + +/** + * Return a deep copy of the given env info. + * + * @param updates - if provided, these values are applied to the copy + */ +export function copyEnvInfo( + env: PythonEnvInfo, + updates?: { + kind?: PythonEnvKind; + }, +): PythonEnvInfo { + // We don't care whether or not extra/hidden properties + // get preserved, so we do the easy thing here. + const copied = cloneDeep(env); + if (updates !== undefined) { + updateEnv(copied, updates); + } + return copied; +} + +function updateEnv( + env: PythonEnvInfo, + updates: { + kind?: PythonEnvKind; + executable?: string; + location?: string; + version?: PythonVersion; + searchLocation?: Uri; + type?: PythonEnvType; + }, +): void { + if (updates.kind !== undefined) { + env.kind = updates.kind; + } + if (updates.executable !== undefined) { + env.executable.filename = updates.executable; + } + if (updates.location !== undefined) { + env.location = updates.location; + } + if (updates.version !== undefined) { + env.version = updates.version; + } + if (updates.searchLocation !== undefined) { + env.searchLocation = updates.searchLocation; + } + if (updates.type !== undefined) { + env.type = updates.type; + } +} + +/** + * Convert the env info to a user-facing representation. + * + * The format is `Python (: )` + * E.g. `Python 3.5.1 32-bit (myenv2: virtualenv)` + */ +export function setEnvDisplayString(env: PythonEnvInfo): void { + env.display = buildEnvDisplayString(env); + env.detailedDisplayName = buildEnvDisplayString(env, true); +} + +function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): string { + // main parts + const shouldDisplayKind = getAllDetails || globallyInstalledEnvKinds.includes(env.kind); + const shouldDisplayArch = !virtualEnvKinds.includes(env.kind); + const displayNameParts: string[] = ['Python']; + if (env.version && !isVersionEmpty(env.version)) { + displayNameParts.push(getVersionDisplayString(env.version)); + } + if (shouldDisplayArch) { + const archName = getArchitectureDisplayName(env.arch); + if (archName !== '') { + displayNameParts.push(archName); + } + } + + // Note that currently we do not use env.distro in the display name. + + // "suffix" + const envSuffixParts: string[] = []; + if (env.name && env.name !== '') { + envSuffixParts.push(`'${env.name}'`); + } else if (env.location && env.location !== '') { + if (env.kind === PythonEnvKind.Conda) { + const condaEnvName = path.basename(env.location); + envSuffixParts.push(`'${condaEnvName}'`); + } + } + if (shouldDisplayKind) { + const kindName = getKindDisplayName(env.kind); + if (kindName !== '') { + envSuffixParts.push(kindName); + } + } + const envSuffix = envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; + + // Pull it all together. + return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); +} + +/** + * For the given data, build a normalized partial info object. + * + * If insufficient data is provided to generate a minimal object, such + * that it is not identifiable, then `undefined` is returned. + */ +function getMinimalPartialInfo(env: string | PythonEnvInfo | BasicEnvInfo): Partial | undefined { + if (typeof env === 'string') { + if (env === '') { + return undefined; + } + return { + id: '', + executable: { + filename: env, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + }; + } + if ('executablePath' in env) { + return { + id: '', + executable: { + filename: env.executablePath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + location: env.envPath, + kind: env.kind, + source: env.source, + }; + } + return env; +} + +/** + * Returns path to environment folder or path to interpreter that uniquely identifies an environment. + */ +export function getEnvPath(interpreterPath: string, envFolderPath?: string): EnvPathType { + let envPath: EnvPathType = { path: interpreterPath, pathType: 'interpreterPath' }; + if (envFolderPath && !isParentPath(interpreterPath, envFolderPath)) { + // Executable is not inside the environment folder, env folder is the ID. + envPath = { path: envFolderPath, pathType: 'envFolderPath' }; + } + return envPath; +} + +/** + * Gets general unique identifier for most environments. + */ +export function getEnvID(interpreterPath: string, envFolderPath?: string): string { + return normCasePath(getEnvPath(interpreterPath, envFolderPath).path); +} + +/** + * Checks if two environments are same. + * @param {string | PythonEnvInfo} left: environment to compare. + * @param {string | PythonEnvInfo} right: environment to compare. + * @param {boolean} allowPartialMatch: allow partial matches of properties when comparing. + * + * Remarks: The current comparison assumes that if the path to the executables are the same + * then it is the same environment. Additionally, if the paths are not same but executables + * are in the same directory and the version of python is the same than we can assume it + * to be same environment. This later case is needed for comparing microsoft store python, + * where multiple versions of python executables are all put in the same directory. + */ +export function areSameEnv( + left: string | PythonEnvInfo | BasicEnvInfo, + right: string | PythonEnvInfo | BasicEnvInfo, + allowPartialMatch = true, +): boolean | undefined { + const leftInfo = getMinimalPartialInfo(left); + const rightInfo = getMinimalPartialInfo(right); + if (leftInfo === undefined || rightInfo === undefined) { + return undefined; + } + if ( + (leftInfo.executable?.filename && !rightInfo.executable?.filename) || + (!leftInfo.executable?.filename && rightInfo.executable?.filename) + ) { + return false; + } + if (leftInfo.id && leftInfo.id === rightInfo.id) { + // In case IDs are available, use it. + return true; + } + + const leftFilename = leftInfo.executable!.filename; + const rightFilename = rightInfo.executable!.filename; + + if (getEnvID(leftFilename, leftInfo.location) === getEnvID(rightFilename, rightInfo.location)) { + // Otherwise use ID function to get the ID. Note ID returned by function may itself change if executable of + // an environment changes, for eg. when conda installs python into the env. So only use it as a fallback if + // ID is not available. + return true; + } + + if (allowPartialMatch) { + const isSameDirectory = + leftFilename !== 'python' && + rightFilename !== 'python' && + arePathsSame(path.dirname(leftFilename), path.dirname(rightFilename)); + if (isSameDirectory) { + const leftVersion = typeof left === 'string' ? undefined : leftInfo.version; + const rightVersion = typeof right === 'string' ? undefined : rightInfo.version; + if (leftVersion && rightVersion) { + if (areIdenticalVersion(leftVersion, rightVersion) || areSimilarVersions(leftVersion, rightVersion)) { + return true; + } + } + } + } + return false; +} + +/** + * Returns a heuristic value on how much information is available in the given version object. + * @param {PythonVersion} version version object to generate heuristic from. + * @returns A heuristic value indicating the amount of info available in the object + * weighted by most important to least important fields. + * Wn > Wn-1 + Wn-2 + ... W0 + */ +function getPythonVersionSpecificity(version: PythonVersion): number { + let infoLevel = 0; + if (version.major > 0) { + infoLevel += 20; // W4 + } + + if (version.minor >= 0) { + infoLevel += 10; // W3 + } + + if (version.micro >= 0) { + infoLevel += 5; // W2 + } + + if (version.release?.level) { + infoLevel += 3; // W1 + } + + if (version.release?.serial || version.sysVersion) { + infoLevel += 1; // W0 + } + + return infoLevel; +} + +/** + * Compares two python versions, based on the amount of data each object has. If versionA has + * less information then the returned value is negative. If it is same then 0. If versionA has + * more information then positive. + */ +export function comparePythonVersionSpecificity(versionA: PythonVersion, versionB: PythonVersion): number { + return Math.sign(getPythonVersionSpecificity(versionA) - getPythonVersionSpecificity(versionB)); +} diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts new file mode 100644 index 000000000000..08f4ce55d464 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '.'; + +/** + * Get the given kind's user-facing representation. + * + * If it doesn't have one then the empty string is returned. + */ +export function getKindDisplayName(kind: PythonEnvKind): string { + for (const [candidate, value] of [ + // Note that Unknown is excluded here. + [PythonEnvKind.System, 'system'], + [PythonEnvKind.MicrosoftStore, 'Microsoft Store'], + [PythonEnvKind.Pyenv, 'pyenv'], + [PythonEnvKind.Poetry, 'Poetry'], + [PythonEnvKind.Hatch, 'Hatch'], + [PythonEnvKind.Pixi, 'Pixi'], + [PythonEnvKind.Custom, 'custom'], + // For now we treat OtherGlobal like Unknown. + [PythonEnvKind.Venv, 'venv'], + [PythonEnvKind.VirtualEnv, 'virtualenv'], + [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], + [PythonEnvKind.Pipenv, 'Pipenv'], + [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'ActiveState'], + // For now we treat OtherVirtual like Unknown. + ] as [PythonEnvKind, string][]) { + if (kind === candidate) { + return value; + } + } + return ''; +} + +/** + * Gets a prioritized list of environment types for identification. + * @returns {PythonEnvKind[]} : List of environments ordered by identification priority + * + * 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. Microsoft Store + * 3. PipEnv + * 4. Pyenv + * 5. Poetry + * 6. Hatch + * 7. Pixi + * + * 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 function getPrioritizedEnvKinds(): PythonEnvKind[] { + return [ + PythonEnvKind.Pyenv, + PythonEnvKind.Pixi, // Placed here since Pixi environments are essentially Conda envs + PythonEnvKind.Conda, + PythonEnvKind.MicrosoftStore, + PythonEnvKind.Pipenv, + PythonEnvKind.Poetry, + PythonEnvKind.Hatch, + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnvWrapper, + PythonEnvKind.VirtualEnv, + PythonEnvKind.ActiveState, + PythonEnvKind.OtherVirtual, + PythonEnvKind.OtherGlobal, + PythonEnvKind.System, + PythonEnvKind.Custom, + PythonEnvKind.Unknown, + ]; +} diff --git a/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/src/client/pythonEnvironments/base/info/environmentInfoService.ts new file mode 100644 index 000000000000..6a981d21b6df --- /dev/null +++ b/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IDisposableRegistry } from '../../../common/types'; +import { createDeferred, Deferred, sleep } from '../../../common/utils/async'; +import { createRunningWorkerPool, IWorkerPool, QueuePosition } from '../../../common/utils/workerPool'; +import { getInterpreterInfo, InterpreterInformation } from './interpreter'; +import { buildPythonExecInfo } from '../../exec'; +import { traceError, traceVerbose, traceWarn } from '../../../logging'; +import { Conda, CONDA_ACTIVATION_TIMEOUT, isCondaEnvironment } from '../../common/environmentManagers/conda'; +import { PythonEnvInfo, PythonEnvKind } from '.'; +import { normCasePath } from '../../common/externalDependencies'; +import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { Architecture } from '../../../common/utils/platform'; +import { getEmptyVersion } from './pythonVersion'; + +export enum EnvironmentInfoServiceQueuePriority { + Default, + High, +} + +export interface IEnvironmentInfoService { + /** + * Get the interpreter information for the given environment. + * @param env The environment to get the interpreter information for. + * @param priority The priority of the request. + */ + getEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + ): Promise; + /** + * Reset any stored interpreter information for the given environment. + * @param searchLocation Search location of the environment. + */ + resetInfo(searchLocation: Uri): void; +} + +async function buildEnvironmentInfo( + env: PythonEnvInfo, + useIsolated = true, +): Promise { + const python = [env.executable.filename]; + if (useIsolated) { + python.push(...['-I', OUTPUT_MARKER_SCRIPT]); + } else { + python.push(...[OUTPUT_MARKER_SCRIPT]); + } + const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(python, undefined, env.executable.filename)); + return interpreterInfo; +} + +async function buildEnvironmentInfoUsingCondaRun(env: PythonEnvInfo): Promise { + const conda = await Conda.getConda(); + const path = env.location.length ? env.location : env.executable.filename; + const condaEnv = await conda?.getCondaEnvironment(path); + if (!condaEnv) { + return undefined; + } + const python = await conda?.getRunPythonArgs(condaEnv, true, true); + if (!python) { + return undefined; + } + const interpreterInfo = await getInterpreterInfo( + buildPythonExecInfo(python, undefined, env.executable.filename), + CONDA_ACTIVATION_TIMEOUT, + ); + return interpreterInfo; +} + +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< + string, + Deferred + >(); + + private workerPool?: IWorkerPool; + + private condaRunWorkerPool?: IWorkerPool; + + public dispose(): void { + if (this.workerPool !== undefined) { + this.workerPool.stop(); + this.workerPool = undefined; + } + if (this.condaRunWorkerPool !== undefined) { + this.condaRunWorkerPool.stop(); + this.condaRunWorkerPool = undefined; + } + } + + public async getEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + ): Promise { + const interpreterPath = env.executable.filename; + const result = this.cache.get(normCasePath(interpreterPath)); + if (result !== undefined) { + // Another call for this environment has already been made, return its result. + return result.promise; + } + + const deferred = createDeferred(); + this.cache.set(normCasePath(interpreterPath), deferred); + this._getEnvironmentInfo(env, priority) + .then((r) => { + deferred.resolve(r); + }) + .catch((ex) => { + deferred.reject(ex); + }); + return deferred.promise; + } + + public async _getEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + retryOnce = true, + ): Promise { + if (env.kind === PythonEnvKind.Conda && env.executable.filename === 'python') { + const emptyInterpreterInfo: InterpreterInformation = { + arch: Architecture.Unknown, + executable: { + filename: 'python', + ctime: -1, + mtime: -1, + sysPrefix: '', + }, + version: getEmptyVersion(), + }; + + return emptyInterpreterInfo; + } + if (this.workerPool === undefined) { + this.workerPool = createRunningWorkerPool( + buildEnvironmentInfo, + ); + } + + let reason: Error | undefined; + let r = await addToQueue(this.workerPool, env, priority).catch((err) => { + reason = err; + return undefined; + }); + + if (r === undefined) { + // Even though env kind is not conda, it can still be a conda environment + // as complete env info may not be available at this time. + const isCondaEnv = env.kind === PythonEnvKind.Conda || (await isCondaEnvironment(env.executable.filename)); + if (isCondaEnv) { + traceVerbose( + `Validating ${env.executable.filename} normally failed with error, falling back to using conda run: (${reason})`, + ); + if (this.condaRunWorkerPool === undefined) { + // Create a separate queue for validation using conda, so getting environment info for + // other types of environment aren't blocked on conda. + this.condaRunWorkerPool = createRunningWorkerPool< + PythonEnvInfo, + InterpreterInformation | undefined + >(buildEnvironmentInfoUsingCondaRun); + } + r = await addToQueue(this.condaRunWorkerPool, env, priority).catch((err) => { + traceError(err); + return undefined; + }); + } else if (reason) { + if ( + reason.message.includes('Unknown option: -I') || + reason.message.includes("ModuleNotFoundError: No module named 'encodings'") + ) { + traceWarn(reason); + if (reason.message.includes('Unknown option: -I')) { + traceError( + 'Support for Python 2.7 has been dropped by the Python extension so certain features may not work, upgrade to using Python 3.', + ); + } + return buildEnvironmentInfo(env, false).catch((err) => { + traceError(err); + return undefined; + }); + } + traceError(reason); + } + } + if (r === undefined && retryOnce) { + // Retry once, in case the environment was not fully populated. Also observed in CI: + // https://github.com/microsoft/vscode-python/issues/20147 where running environment the first time + // failed due to unknown reasons. + return sleep(2000).then(() => this._getEnvironmentInfo(env, priority, false)); + } + return r; + } + + public resetInfo(searchLocation: Uri): void { + const searchLocationPath = searchLocation.fsPath; + const keys = Array.from(this.cache.keys()); + keys.forEach((key) => { + if (key.startsWith(normCasePath(searchLocationPath))) { + this.cache.delete(key); + } + }); + } +} + +function addToQueue( + workerPool: IWorkerPool, + env: PythonEnvInfo, + priority: EnvironmentInfoServiceQueuePriority | undefined, +) { + return priority === EnvironmentInfoServiceQueuePriority.High + ? workerPool.addToQueue(env, QueuePosition.Front) + : workerPool.addToQueue(env, QueuePosition.Back); +} + +let envInfoService: IEnvironmentInfoService | undefined; +export function getEnvironmentInfoService(disposables?: IDisposableRegistry): IEnvironmentInfoService { + if (envInfoService === undefined) { + const service = new EnvironmentInfoService(); + disposables?.push({ + dispose: () => { + service.dispose(); + envInfoService = undefined; + }, + }); + envInfoService = service; + } + return envInfoService; +} diff --git a/src/client/pythonEnvironments/base/info/executable.ts b/src/client/pythonEnvironments/base/info/executable.ts new file mode 100644 index 000000000000..ab5a67d79315 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/executable.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { getOSType, OSType } from '../../../common/utils/platform'; +import { getEmptyVersion, parseVersion } from './pythonVersion'; + +import { PythonVersion } from '.'; +import { normCasePath } from '../../common/externalDependencies'; + +/** + * Determine a best-effort Python version based on the given filename. + */ +export function parseVersionFromExecutable(filename: string): PythonVersion { + const version = parseBasename(path.basename(filename)); + + if (version.major === 2 && version.minor === -1) { + version.minor = 7; + } + + return version; +} + +function parseBasename(basename: string): PythonVersion { + basename = normCasePath(basename); + if (getOSType() === OSType.Windows) { + if (basename === 'python.exe') { + // On Windows we can't assume it is 2.7. + return getEmptyVersion(); + } + } else if (basename === 'python') { + // We can assume it is 2.7. (See PEP 394.) + return parseVersion('2.7'); + } + if (!basename.startsWith('python')) { + throw Error(`not a Python executable (expected "python..", got "${basename}")`); + } + // If we reach here then we expect it to have a version in the name. + return parseVersion(basename); +} diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts new file mode 100644 index 000000000000..4547e7606308 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -0,0 +1,229 @@ +// 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', + MicrosoftStore = 'global-microsoft-store', + Pyenv = 'global-pyenv', + Poetry = 'poetry', + Hatch = 'hatch', + Pixi = 'pixi', + ActiveState = 'activestate', + Custom = 'global-custom', + OtherGlobal = 'global-other', + // "virtual" + Venv = 'virt-venv', + VirtualEnv = 'virt-virtualenv', + VirtualEnvWrapper = 'virt-virtualenvwrapper', + Pipenv = 'virt-pipenv', + Conda = 'virt-conda', + OtherVirtual = 'virt-other', +} + +export enum PythonEnvType { + Conda = 'Conda', + Virtual = 'Virtual', +} + +export interface EnvPathType { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path: string; + pathType: 'envFolderPath' | 'interpreterPath'; +} + +export const virtualEnvKinds = [ + PythonEnvKind.Poetry, + PythonEnvKind.Hatch, + PythonEnvKind.Pixi, + PythonEnvKind.Pipenv, + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnvWrapper, + PythonEnvKind.Conda, + PythonEnvKind.VirtualEnv, +]; + +export const globallyInstalledEnvKinds = [ + PythonEnvKind.OtherGlobal, + PythonEnvKind.Unknown, + PythonEnvKind.MicrosoftStore, + PythonEnvKind.System, + PythonEnvKind.Custom, +]; + +/** + * Information about a file. + */ +export type FileInfo = { + filename: string; + ctime: number; + mtime: number; +}; + +/** + * Information about a Python binary/executable. + */ +export type PythonExecutableInfo = FileInfo & { + sysPrefix: string; +}; + +/** + * Source types indicating how a particular environment was discovered. + * + * Notes: This is used in auto-selection to figure out which python to select. + * We added this field to support the existing mechanism in the extension to + * calculate the auto-select python. + */ +export enum PythonEnvSource { + /** + * Environment was found via PATH env variable + */ + PathEnvVar = 'path env var', + /** + * Environment was found in windows registry + */ + WindowsRegistry = 'windows registry', + // If source turns out to be useful we will expand this enum to contain more details sources. +} + +/** + * 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 + * @prop source - the locator[s] which found the environment. + */ +type PythonEnvBaseInfo = { + id?: string; + kind: PythonEnvKind; + type?: PythonEnvType; + 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) + + source: PythonEnvSource[]; +}; + +/** + * 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. + */ +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 + */ +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 display - the text to use when showing the env to users + * @prop detailedDisplayName - display name containing all details + * @prop searchLocation - the project to which this env is related to, if any + */ +export type PythonEnvInfo = _PythonEnvInfo & { + distro: PythonDistroInfo; + display?: string; + detailedDisplayName?: string; + searchLocation?: Uri; + /** + * Command used to run Python in this environment. + * E.g. `conda run -n envName python` or `python.exe` + */ + pythonRunCommand?: string[]; + identifiedUsingNativeLocator?: boolean; +}; + +/** + * A dummy python version object containing default fields. + * + * Note this object is immutable. So if it is assigned to another object, the properties of the other object + * also cannot be modified by reference. For eg. `otherVersionObject.major = 3` won't work. + */ +export const UNKNOWN_PYTHON_VERSION: PythonVersion = { + major: -1, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Final, serial: -1 }, + sysVersion: undefined, +}; +Object.freeze(UNKNOWN_PYTHON_VERSION); diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts new file mode 100644 index 000000000000..e19e1f0d45c2 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonExecutableInfo, PythonVersion } from '.'; +import { isCI } from '../../../common/constants'; +import { + interpreterInfo as getInterpreterInfoCommand, + InterpreterInfoJson, +} from '../../../common/process/internal/scripts'; +import { Architecture } from '../../../common/utils/platform'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { shellExecute } from '../../common/externalDependencies'; +import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; +import { parseVersion } from './pythonVersion'; + +export type InterpreterInformation = { + arch: Architecture; + executable: PythonExecutableInfo; + version: 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 + */ +function extractInterpreterInfo(python: string, raw: InterpreterInfoJson): InterpreterInformation { + let rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}`; + + // We only need additional version details if the version is 'alpha', 'beta' or 'candidate'. + // This restriction is needed to avoid sending any PII if this data is used with telemetry. + // With custom builds of python it is possible that release level and values after that can + // contain PII. + if (raw.versionInfo[3] !== undefined && ['final', 'alpha', 'beta', 'candidate'].includes(raw.versionInfo[3])) { + rawVersion = `${rawVersion}-${raw.versionInfo[3]}`; + if (raw.versionInfo[4] !== undefined) { + let serial = -1; + try { + serial = parseInt(`${raw.versionInfo[4]}`, 10); + } catch (ex) { + serial = -1; + } + rawVersion = serial >= 0 ? `${rawVersion}${serial}` : rawVersion; + } + } + return { + arch: raw.is64Bit ? Architecture.x64 : Architecture.x86, + executable: { + filename: python, + sysPrefix: raw.sysPrefix, + mtime: -1, + ctime: -1, + }, + version: { + ...parseVersion(rawVersion), + sysVersion: raw.sysVersion, + }, + }; +} + +/** + * Collect full interpreter information from the given Python executable. + * + * @param python - the information to use when running Python + * @param timeout - any specific timeouts to use for getting info. + */ +export async function getInterpreterInfo( + python: PythonExecInfo, + timeout?: number, +): 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.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); + + // Sometimes on CI, the python process takes a long time to start up. This is a workaround for that. + let standardTimeout = isCI ? 30000 : 15000; + if (process.env.VSC_PYTHON_INTERPRETER_INFO_TIMEOUT !== undefined) { + // Custom override for setups where the initial Python setup process may take longer than the standard timeout. + standardTimeout = parseInt(process.env.VSC_PYTHON_INTERPRETER_INFO_TIMEOUT, 10); + traceInfo(`Custom interpreter discovery timeout: ${standardTimeout}`); + } + + // 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 shellExecute(quoted, { timeout: timeout ?? standardTimeout }); + if (result.stderr) { + traceError( + `Stderr when executing script with >> ${quoted} << stderr: ${result.stderr}, still attempting to parse output`, + ); + } + let json: InterpreterInfoJson; + try { + json = parse(result.stdout); + } catch (ex) { + traceError(`Failed to parse interpreter information for >> ${quoted} << with ${ex}`); + return undefined; + } + traceVerbose(`Found interpreter for >> ${quoted} <<: ${JSON.stringify(json)}`); + return extractInterpreterInfo(python.pythonExecutable, json); +} diff --git a/src/client/pythonEnvironments/base/info/pythonVersion.ts b/src/client/pythonEnvironments/base/info/pythonVersion.ts new file mode 100644 index 000000000000..589bf4c7b7af --- /dev/null +++ b/src/client/pythonEnvironments/base/info/pythonVersion.ts @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import * as basic from '../../../common/utils/version'; + +import { PythonReleaseLevel, PythonVersion, PythonVersionRelease, UNKNOWN_PYTHON_VERSION } from '.'; +import { traceError } from '../../../logging'; + +// XXX getPythonVersionFromPath() should go away in favor of parseVersionFromExecutable(). + +export function getPythonVersionFromPath(exe: string): PythonVersion { + let version = UNKNOWN_PYTHON_VERSION; + try { + version = parseVersion(path.basename(exe)); + } catch (ex) { + traceError(`Failed to parse version from path: ${exe}`, ex); + } + return version; +} + +/** + * Convert the given string into the corresponding Python version object. + * + * Example: + * 3.9.0 + * 3.9.0a1 + * 3.9.0b2 + * 3.9.0rc1 + * 3.9.0-beta2 + * 3.9.0.beta.2 + * 3.9.0.final.0 + * 39 + */ +export function parseVersion(versionStr: string): PythonVersion { + const [version, after] = parseBasicVersion(versionStr); + if (version.micro === -1) { + return version; + } + const [release] = parseRelease(after); + version.release = release; + return version; +} + +export function parseRelease(text: string): [PythonVersionRelease | undefined, string] { + let after: string; + + let alpha: string | undefined; + let beta: string | undefined; + let rc: string | undefined; + let fin: string | undefined; + let serialStr: string; + + let match = text.match(/^(?:-?final|\.final(?:\.0)?)(.*)$/); + if (match) { + [, after] = match; + fin = 'final'; + serialStr = '0'; + } else { + for (const regex of [ + /^(?:(a)|(b)|(rc))([1-9]\d*)(.*)$/, + /^-(?:(?:(alpha)|(beta)|(candidate))([1-9]\d*))(.*)$/, + /^\.(?:(?:(alpha)|(beta)|(candidate))\.([1-9]\d*))(.*)$/, + ]) { + match = text.match(regex); + if (match) { + [, alpha, beta, rc, serialStr, after] = match; + break; + } + } + } + + let level: PythonReleaseLevel; + if (fin) { + level = PythonReleaseLevel.Final; + } else if (rc) { + level = PythonReleaseLevel.Candidate; + } else if (beta) { + level = PythonReleaseLevel.Beta; + } else if (alpha) { + level = PythonReleaseLevel.Alpha; + } else { + // We didn't find release info. + return [undefined, text]; + } + const serial = parseInt(serialStr!, 10); + return [{ level, serial }, after!]; +} + +/** + * Convert the given string into the corresponding Python version object. + */ +export function parseBasicVersion(versionStr: string): [PythonVersion, string] { + // We set a prefix (which will be ignored) to make sure "plain" + // versions are fully parsed. + const parsed = basic.parseBasicVersionInfo(`ignored-${versionStr}`); + if (!parsed) { + if (versionStr === '') { + return [getEmptyVersion(), '']; + } + throw Error(`invalid version ${versionStr}`); + } + // We ignore any "before" text. + const { version, after } = parsed; + version.release = undefined; + + if (version.minor === -1) { + // We trust that the major version is always single-digit. + if (version.major > 9) { + const numdigits = version.major.toString().length - 1; + const factor = 10 ** numdigits; + version.minor = version.major % factor; + version.major = Math.floor(version.major / factor); + } + } + + return [version, after]; +} + +/** + * Get a new version object with all properties "zeroed out". + */ +export function getEmptyVersion(): PythonVersion { + return cloneDeep(basic.EMPTY_VERSION); +} + +/** + * Determine if the version is effectively a blank one. + */ +export function isVersionEmpty(version: PythonVersion): boolean { + // We really only care the `version.major` is -1. However, using + // generic util is better in the long run. + return basic.isVersionInfoEmpty(version); +} +/** + * Convert the info to a user-facing representation. + */ +export function getVersionDisplayString(ver: PythonVersion): string { + if (isVersionEmpty(ver)) { + return ''; + } + if (ver.micro !== -1) { + return getShortVersionString(ver); + } + return `${getShortVersionString(ver)}.x`; +} + +/** + * Convert the info to a simple string. + */ +export function getShortVersionString(ver: PythonVersion): string { + let verStr = basic.getVersionString(ver); + if (ver.release === undefined) { + return verStr; + } + if (ver.release.level === PythonReleaseLevel.Final) { + return verStr; + } + if (ver.release.level === PythonReleaseLevel.Candidate) { + verStr = `${verStr}rc${ver.release.serial}`; + } else if (ver.release.level === PythonReleaseLevel.Beta) { + verStr = `${verStr}b${ver.release.serial}`; + } else if (ver.release.level === PythonReleaseLevel.Alpha) { + verStr = `${verStr}a${ver.release.serial}`; + } else { + throw Error(`unsupported release level ${ver.release.level}`); + } + return verStr; +} + +/** + * Checks if all the important properties of the version objects match. + * + * Only major, minor, micro, and release are compared. + */ +export function areIdenticalVersion(left: PythonVersion, right: PythonVersion): boolean { + return basic.areIdenticalVersion(left, right, compareVersionRelease); +} + +/** + * Checks if the versions are identical or one is more complete than other (and otherwise the same). + * + * A `true` result means the Python executables are strictly compatible. + * For Python 3+, at least the minor version must be set. `(2, -1, -1)` + * implies 2.7, so in that case only the major version must be set (to 2). + */ +export function areSimilarVersions(left: PythonVersion, right: PythonVersion): boolean { + if (!basic.areSimilarVersions(left, right, compareVersionRelease)) { + return false; + } + if (left.major === 2) { + return true; + } + return left.minor > -1 && right.minor > -1; +} + +function compareVersionRelease(left: PythonVersion, right: PythonVersion): [number, string] { + if (left.release === undefined) { + if (right.release === undefined) { + return [0, '']; + } + return [1, 'level']; + } + if (right.release === undefined) { + return [-1, 'level']; + } + + // Compare the level. + if (left.release.level < right.release.level) { + return [1, 'level']; + } + if (left.release.level > right.release.level) { + return [-1, 'level']; + } + if (left.release.level === PythonReleaseLevel.Final) { + // We ignore "serial". + return [0, '']; + } + + // Compare the serial. + if (left.release.serial < right.release.serial) { + return [1, 'serial']; + } + if (left.release.serial > right.release.serial) { + return [-1, 'serial']; + } + + return [0, '']; +} + +/** + * Convert Python version to semver like version object. + * + * Remarks: primarily used to convert to old type of environment info. + * @deprecated + */ +export function toSemverLikeVersion( + version: PythonVersion, +): { + raw: string; + major: number; + minor: number; + patch: number; + build: string[]; + prerelease: string[]; +} { + const versionPrefix = basic.getVersionString(version); + let preRelease: string[] = []; + if (version.release) { + preRelease = + version.release.serial < 0 + ? [`${version.release.level}`] + : [`${version.release.level}`, `${version.release.serial}`]; + } + return { + raw: versionPrefix, + major: version.major, + minor: version.minor, + patch: version.micro, + build: [], + prerelease: preRelease, + }; +} + +/** + * Compares major, minor, patch for two versions of python + * @param v1 : semVer like version object + * @param v2 : semVer like version object + * @returns {1 | 0 | -1} : 0 if v1 === v2, + * 1 if v1 > v2, + * -1 if v1 < v2 + * Remarks: primarily used compare to old type of version info. + * @deprecated + */ +export function compareSemVerLikeVersions( + v1: { major: number; minor: number; patch: number }, + v2: { major: number; minor: number; patch: number }, +): 1 | 0 | -1 { + if (v1.major === v2.major) { + if (v1.minor === v2.minor) { + if (v1.patch === v2.patch) { + return 0; + } + return v1.patch > v2.patch ? 1 : -1; + } + return v1.minor > v2.minor ? 1 : -1; + } + return v1.major > v2.major ? 1 : -1; +} diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts new file mode 100644 index 000000000000..0c15f8b27e5f --- /dev/null +++ b/src/client/pythonEnvironments/base/locator.ts @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable max-classes-per-file */ + +import { Event, Uri } from 'vscode'; +import { IAsyncIterableIterator, iterEmpty } from '../../common/utils/async'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvSource, PythonVersion } from './info'; +import { + IPythonEnvsWatcher, + PythonEnvCollectionChangedEvent, + PythonEnvsChangedEvent, + PythonEnvsWatcher, +} from './watcher'; +import type { Architecture } from '../../common/utils/platform'; + +/** + * A single update to a previously provided Python env object. + */ +export type PythonEnvUpdatedEvent = { + /** + * The iteration index of The env info that was previously provided. + */ + index: number; + /** + * The env info that was previously provided. + */ + old?: I; + /** + * The env info that replaces the old info. + * Update is sent as `undefined` if we find out that the environment is no longer valid. + */ + update: I | undefined; +}; + +/** + * A fast async iterator of Python envs, which may have incomplete info. + * + * Each object yielded by the iterator represents a unique Python + * environment. + * + * The iterator is not required to have provide all info about + * an environment. However, each yielded item will at least + * include all the `PythonEnvBaseInfo` data. + * + * During iteration the information for an already + * yielded object may be updated. Rather than updating the yielded + * object or yielding it again with updated info, the update is + * emitted by the iterator's `onUpdated` (event) property. Once there are no more updates, the event emits + * `null`. + * + * If the iterator does not have `onUpdated` then it means the + * provider does not support updates. + * + * Callers can usually ignore the update event entirely and rely on + * the locator to provide sufficiently complete information. + */ +export interface IPythonEnvsIterator extends IAsyncIterableIterator { + /** + * Provides possible updates for already-iterated envs. + * + * Once there are no more updates, `null` is emitted. + * + * If this property is not provided then it means the iterator does + * not support updates. + */ + onUpdated?: Event | ProgressNotificationEvent>; +} + +export enum ProgressReportStage { + idle = 'idle', + discoveryStarted = 'discoveryStarted', + allPathsDiscovered = 'allPathsDiscovered', + discoveryFinished = 'discoveryFinished', +} + +export type ProgressNotificationEvent = { + stage: ProgressReportStage; +}; + +export function isProgressEvent( + event: PythonEnvUpdatedEvent | ProgressNotificationEvent, +): event is ProgressNotificationEvent { + return 'stage' in event; +} + +/** + * An empty Python envs iterator. + */ +export const NOOP_ITERATOR: IPythonEnvsIterator = iterEmpty(); + +/** + * The most basic info to send to a locator when requesting environments. + * + * This is directly correlated with the `BasicPythonEnvsChangedEvent` + * emitted by watchers. + */ +type BasicPythonLocatorQuery = { + /** + * If provided, results should be limited to these env + * kinds; if not provided, the kind of each environment + * is not considered when filtering + */ + kinds?: PythonEnvKind[]; +}; + +/** + * The portion of a query related to env search locations. + */ +type SearchLocations = { + /** + * The locations under which to look for environments. + */ + roots: Uri[]; + /** + * If true, only query for workspace related envs, i.e do not look for environments that do not have a search location. + */ + doNotIncludeNonRooted?: boolean; +}; + +/** + * The full set of possible info to send to a locator when requesting environments. + * + * This is directly correlated with the `PythonEnvsChangedEvent` + * emitted by watchers. + */ +export type PythonLocatorQuery = BasicPythonLocatorQuery & { + /** + * If provided, results should be limited to within these locations. + */ + searchLocations?: SearchLocations; + /** + * If provided, results should be limited envs provided by these locators. + */ + providerId?: string; + /** + * If provided, results are limited to this env. + */ + envPath?: string; +}; + +type QueryForEvent = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery; + +export type BasicEnvInfo = { + kind: PythonEnvKind; + executablePath: string; + source?: PythonEnvSource[]; + envPath?: string; + /** + * The project to which this env is related to, if any + * E.g. the project directory when dealing with pipenv virtual environments. + */ + searchLocation?: Uri; + version?: PythonVersion; + name?: string; + /** + * Display name provided by locators, not generated by us. + * E.g. display name as provided by Windows Registry or Windows Store, etc + */ + displayName?: string; + identifiedUsingNativeLocator?: boolean; + arch?: Architecture; + ctime?: number; + mtime?: number; +}; + +/** + * 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 { + readonly providerId: string; + /** + * 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. To ensure all + * possible information is filled in, call `ILocator.resolveEnv()`. + * + * Updates to yielded objects may be provided via the optional + * `onUpdated` property of the iterator. However, callers can + * usually ignore the update event entirely and rely on the + * locator to provide sufficiently complete information. + * + * @param query - if provided, the locator will limit results to match + * @returns - the fast async iterator of Python envs, which may have incomplete info + */ + iterEnvs(query?: QueryForEvent): IPythonEnvsIterator; +} + +export type ICompositeLocator = Omit, 'providerId'>; + +interface IResolver { + /** + * Find as much info about the given Python environment as possible. + * If path passed is invalid, then `undefined` is returned. + * + * @param path - Python executable path or environment path to resolve more information about + */ + resolveEnv(path: string): Promise; +} + +export interface IResolvingLocator extends IResolver, ICompositeLocator {} + +export interface GetRefreshEnvironmentsOptions { + /** + * Get refresh promise which resolves once the following stage has been reached for the list of known environments. + */ + stage?: ProgressReportStage; +} + +export type TriggerRefreshOptions = { + /** + * Only trigger a refresh if it hasn't already been triggered for this session. + */ + ifNotTriggerredAlready?: boolean; +}; + +export interface IDiscoveryAPI { + readonly refreshState: ProgressReportStage; + /** + * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant + * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of + * the entire collection. + */ + readonly onProgress: Event; + /** + * Fires with details if the known list changes. + */ + readonly onChanged: Event; + /** + * Resolves once environment list has finished refreshing, i.e all environments are + * discovered. Carries `undefined` if there is no refresh currently going on. + */ + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + /** + * Triggers a new refresh for query if there isn't any already running. + */ + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; + /** + * Get current list of known environments. + */ + getEnvs(query?: PythonLocatorQuery): PythonEnvInfo[]; + /** + * Find as much info about the given Python environment as possible. + * If path passed is invalid, then `undefined` is returned. + * + * @param path - Full path of Python executable or environment folder to resolve more information about + */ + resolveEnv(path: string): Promise; +} + +export 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`. + */ +abstract class LocatorBase implements ILocator { + public readonly onChanged: Event; + + public abstract readonly providerId: string; + + protected readonly emitter: IEmitter; + + constructor(watcher: IPythonEnvsWatcher & IEmitter) { + this.emitter = watcher; + this.onChanged = watcher.onChanged; + } + + // eslint-disable-next-line class-methods-use-this + public abstract iterEnvs(query?: QueryForEvent): IPythonEnvsIterator; +} + +/** + * 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/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts new file mode 100644 index 000000000000..6af8c0ee1b69 --- /dev/null +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { createDeferred } from '../../common/utils/async'; +import { getURIFilter } from '../../common/utils/misc'; +import { traceVerbose } from '../../logging'; +import { PythonEnvInfo } from './info'; +import { + IPythonEnvsIterator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from './locator'; + +/** + * Create a filter function to match the given query. + */ +export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) => boolean { + const kinds = query.kinds !== undefined && query.kinds.length > 0 ? query.kinds : undefined; + const includeNonRooted = !query.searchLocations?.doNotIncludeNonRooted; // We default to `true`. + const locationFilters = getSearchLocationFilters(query); + function checkKind(env: PythonEnvInfo): boolean { + if (kinds === undefined) { + return true; + } + return kinds.includes(env.kind); + } + function checkSearchLocation(env: PythonEnvInfo): boolean { + if (env.searchLocation === undefined) { + // It is not a "rooted" env. + return includeNonRooted; + } + // It is a "rooted" env. + const loc = env.searchLocation; + if (locationFilters !== undefined) { + // Check against the requested roots. (There may be none.) + return locationFilters.some((filter) => filter(loc)); + } + return true; + } + return (env) => { + if (!checkKind(env)) { + return false; + } + if (!checkSearchLocation(env)) { + return false; + } + return true; + }; +} + +function getSearchLocationFilters(query: PythonLocatorQuery): ((u: Uri) => boolean)[] | undefined { + if (query.searchLocations === undefined) { + return undefined; + } + if (query.searchLocations.roots.length === 0) { + return []; + } + return query.searchLocations.roots.map((loc) => + getURIFilter(loc, { + checkParent: true, + }), + ); +} + +/** + * Unroll the given iterator into an array. + * + * This includes applying any received updates. + */ +export async function getEnvs(iterator: IPythonEnvsIterator): Promise { + const envs: (I | undefined)[] = []; + + const updatesDone = createDeferred(); + if (iterator.onUpdated === undefined) { + updatesDone.resolve(); + } else { + const listener = iterator.onUpdated((event: PythonEnvUpdatedEvent | ProgressNotificationEvent) => { + if (isProgressEvent(event)) { + if (event.stage !== ProgressReportStage.discoveryFinished) { + return; + } + updatesDone.resolve(); + listener.dispose(); + } else if (event.index !== undefined) { + const { index, update } = event; + if (envs[index] === undefined) { + const json = JSON.stringify(update); + traceVerbose( + `Updates sent for an env which was classified as invalid earlier, currently not expected, ${json}`, + ); + } + // We don't worry about if envs[index] is set already. + envs[index] = update; + } + }); + } + + let itemIndex = 0; + for await (const env of iterator) { + // We can't just push because updates might get emitted early. + if (envs[itemIndex] === undefined) { + envs[itemIndex] = env; + } + itemIndex += 1; + } + await updatesDone.promise; + + // Do not return invalid environments + return envs.filter((e) => e !== undefined).map((e) => e!); +} diff --git a/src/client/pythonEnvironments/base/locators.ts b/src/client/pythonEnvironments/base/locators.ts new file mode 100644 index 000000000000..10be15c27bf1 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { chain } from '../../common/utils/async'; +import { Disposables } from '../../common/utils/resourceLifecycle'; +import { PythonEnvInfo } from './info'; +import { + ICompositeLocator, + ILocator, + IPythonEnvsIterator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from './locator'; +import { PythonEnvsWatchers } from './watchers'; + +/** + * Combine the `onUpdated` event of the given iterators into a single event. + */ +export function combineIterators(iterators: IPythonEnvsIterator[]): IPythonEnvsIterator { + const result: IPythonEnvsIterator = chain(iterators); + const events = iterators.map((it) => it.onUpdated).filter((v) => v); + if (!events || events.length === 0) { + // There are no sub-events, so we leave `onUpdated` undefined. + return result; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result.onUpdated = (handleEvent: (e: PythonEnvUpdatedEvent | ProgressNotificationEvent) => any) => { + const disposables = new Disposables(); + let numActive = events.length; + events.forEach((event) => { + const disposable = event!((e: PythonEnvUpdatedEvent | ProgressNotificationEvent) => { + // NOSONAR + if (isProgressEvent(e)) { + if (e.stage === ProgressReportStage.discoveryFinished) { + numActive -= 1; + if (numActive === 0) { + // All the sub-events are done so we're done. + handleEvent({ stage: ProgressReportStage.discoveryFinished }); + } + } else { + handleEvent({ stage: e.stage }); + } + } else { + handleEvent(e); + } + }); + disposables.push(disposable); + }); + return disposables; + }; + return result; +} + +/** + * 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 ICompositeLocator { + public readonly providerId: string; + + constructor( + // The locators will be watched as well as iterated. + private readonly locators: ReadonlyArray>, + ) { + super(locators); + this.providerId = locators.map((loc) => loc.providerId).join('+'); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const iterators = this.locators.map((loc) => loc.iterEnvs(query)); + return combineIterators(iterators); + } +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts new file mode 100644 index 000000000000..ea0d63cd7552 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts @@ -0,0 +1,541 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, EventEmitter, Event, Uri } from 'vscode'; +import * as ch from 'child_process'; +import * as path from 'path'; +import * as rpc from 'vscode-jsonrpc/node'; +import { PassThrough } from 'stream'; +import * as fs from '../../../../common/platform/fs-paths'; +import { isWindows, getUserHomeDir } from '../../../../common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../../constants'; +import { createDeferred, createDeferredFrom } from '../../../../common/utils/async'; +import { DisposableBase, DisposableStore } from '../../../../common/utils/resourceLifecycle'; +import { noop } from '../../../../common/utils/misc'; +import { getConfiguration, getWorkspaceFolderPaths, isTrusted } from '../../../../common/vscodeApis/workspaceApis'; +import { CONDAPATH_SETTING_KEY } from '../../../common/environmentManagers/conda'; +import { VENVFOLDERS_SETTING_KEY, VENVPATH_SETTING_KEY } from '../lowLevel/customVirtualEnvLocator'; +import { createLogOutputChannel, showWarningMessage } from '../../../../common/vscodeApis/windowApis'; +import { sendNativeTelemetry, NativePythonTelemetry } from './nativePythonTelemetry'; +import { NativePythonEnvironmentKind } from './nativePythonUtils'; +import type { IExtensionContext } from '../../../../common/types'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; +import { traceError } from '../../../../logging'; +import { Common, PythonLocator } from '../../../../common/utils/localize'; +import { Commands } from '../../../../common/constants'; +import { executeCommand } from '../../../../common/vscodeApis/commandApis'; +import { getGlobalStorage, IPersistentStorage } from '../../../../common/persistentState'; + +const PYTHON_ENV_TOOLS_PATH = isWindows() + ? path.join(EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet.exe') + : path.join(EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet'); + +const DONT_SHOW_SPAWN_ERROR_AGAIN = 'DONT_SHOW_NATIVE_FINDER_SPAWN_ERROR_AGAIN'; + +export interface NativeEnvInfo { + displayName?: string; + name?: string; + executable?: string; + kind?: NativePythonEnvironmentKind; + version?: string; + prefix?: string; + manager?: NativeEnvManagerInfo; + /** + * Path to the project directory when dealing with pipenv virtual environments. + */ + project?: string; + arch?: 'x64' | 'x86'; + symlinks?: string[]; +} + +export interface NativeEnvManagerInfo { + tool: string; + executable: string; + version?: string; +} + +export function isNativeEnvInfo(info: NativeEnvInfo | NativeEnvManagerInfo): info is NativeEnvInfo { + if ((info as NativeEnvManagerInfo).tool) { + return false; + } + return true; +} + +export type NativeCondaInfo = { + canSpawnConda: boolean; + userProvidedEnvFound?: boolean; + condaRcs: string[]; + envDirs: string[]; + environmentsTxt?: string; + environmentsTxtExists?: boolean; + environmentsFromTxt: string[]; +}; + +export interface NativePythonFinder extends Disposable { + /** + * Refresh the list of python environments. + * Returns an async iterable that can be used to iterate over the list of python environments. + * Internally this will take all of the current workspace folders and search for python environments. + * + * If a Uri is provided, then it will search for python environments in that location (ignoring workspaces). + * Uri can be a file or a folder. + * If a NativePythonEnvironmentKind is provided, then it will search for python environments of that kind (ignoring workspaces). + */ + refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable; + /** + * Will spawn the provided Python executable and return information about the environment. + * @param executable + */ + resolve(executable: string): Promise; + /** + * Used only for telemetry. + */ + getCondaInfo(): Promise; +} + +interface NativeLog { + level: string; + message: string; +} + +class NativePythonFinderImpl extends DisposableBase implements NativePythonFinder { + private readonly connection: rpc.MessageConnection; + + private firstRefreshResults: undefined | (() => AsyncGenerator); + + private readonly outputChannel = this._register(createLogOutputChannel('Python Locator', { log: true })); + + private initialRefreshMetrics = { + timeToSpawn: 0, + timeToConfigure: 0, + timeToRefresh: 0, + }; + + private readonly suppressErrorNotification: IPersistentStorage; + + constructor(private readonly cacheDirectory?: Uri, private readonly context?: IExtensionContext) { + super(); + this.suppressErrorNotification = this.context + ? getGlobalStorage(this.context, DONT_SHOW_SPAWN_ERROR_AGAIN, false) + : ({ get: () => false, set: async () => {} } as IPersistentStorage); + this.connection = this.start(); + void this.configure(); + this.firstRefreshResults = this.refreshFirstTime(); + } + + public async resolve(executable: string): Promise { + await this.configure(); + const environment = await this.connection.sendRequest('resolve', { + executable, + }); + + this.outputChannel.info(`Resolved Python Environment ${environment.executable}`); + return environment; + } + + async *refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable { + if (this.firstRefreshResults) { + // If this is the first time we are refreshing, + // Then get the results from the first refresh. + // Those would have started earlier and cached in memory. + const results = this.firstRefreshResults(); + this.firstRefreshResults = undefined; + yield* results; + } else { + const result = this.doRefresh(options); + let completed = false; + void result.completed.finally(() => { + completed = true; + }); + const envs: (NativeEnvInfo | NativeEnvManagerInfo)[] = []; + let discovered = createDeferred(); + const disposable = result.discovered((data) => { + envs.push(data); + discovered.resolve(); + }); + do { + if (!envs.length) { + await Promise.race([result.completed, discovered.promise]); + } + if (envs.length) { + const dataToSend = [...envs]; + envs.length = 0; + for (const data of dataToSend) { + yield data; + } + } + if (!completed) { + discovered = createDeferred(); + } + } while (!completed); + disposable.dispose(); + } + } + + refreshFirstTime() { + const result = this.doRefresh(); + const completed = createDeferredFrom(result.completed); + const envs: NativeEnvInfo[] = []; + let discovered = createDeferred(); + const disposable = result.discovered((data) => { + envs.push(data); + discovered.resolve(); + }); + + const iterable = async function* () { + do { + if (!envs.length) { + await Promise.race([completed.promise, discovered.promise]); + } + if (envs.length) { + const dataToSend = [...envs]; + envs.length = 0; + for (const data of dataToSend) { + yield data; + } + } + if (!completed.completed) { + discovered = createDeferred(); + } + } while (!completed.completed); + disposable.dispose(); + }; + + return iterable.bind(this); + } + + // eslint-disable-next-line class-methods-use-this + private start(): rpc.MessageConnection { + this.outputChannel.info(`Starting Python Locator ${PYTHON_ENV_TOOLS_PATH} server`); + + // jsonrpc package cannot handle messages coming through too quickly. + // Lets handle the messages and close the stream only when + // we have got the exit event. + const readable = new PassThrough(); + const writable = new PassThrough(); + const disposables: Disposable[] = []; + try { + const stopWatch = new StopWatch(); + const proc = ch.spawn(PYTHON_ENV_TOOLS_PATH, ['server'], { env: process.env }); + this.initialRefreshMetrics.timeToSpawn = stopWatch.elapsedTime; + proc.stdout.pipe(readable, { end: false }); + proc.stderr.on('data', (data) => this.outputChannel.error(data.toString())); + writable.pipe(proc.stdin, { end: false }); + + // Handle spawn errors (e.g., missing DLLs on Windows) + proc.on('error', (error) => { + this.outputChannel.error(`Python Locator process error: ${error.message}`); + this.outputChannel.error(`Error details: ${JSON.stringify(error)}`); + this.handleSpawnError(error.message); + }); + + // Handle immediate exits with error codes + let hasStarted = false; + setTimeout(() => { + hasStarted = true; + }, 1000); + + proc.on('exit', (code, signal) => { + if (!hasStarted && code !== null && code !== 0) { + const errorMessage = `Python Locator process exited immediately with code ${code}`; + this.outputChannel.error(errorMessage); + if (signal) { + this.outputChannel.error(`Exit signal: ${signal}`); + } + this.handleSpawnError(errorMessage); + } + }); + + disposables.push({ + dispose: () => { + try { + if (proc.exitCode === null) { + proc.kill(); + } + } catch (ex) { + this.outputChannel.error('Error disposing finder', ex); + } + }, + }); + } catch (ex) { + this.outputChannel.error(`Error starting Python Finder ${PYTHON_ENV_TOOLS_PATH} server`, ex); + } + const disposeStreams = new Disposable(() => { + readable.end(); + writable.end(); + }); + const connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(readable), + new rpc.StreamMessageWriter(writable), + ); + disposables.push( + connection, + disposeStreams, + connection.onError((ex) => { + disposeStreams.dispose(); + this.outputChannel.error('Connection Error:', ex); + }), + connection.onNotification('log', (data: NativeLog) => { + switch (data.level) { + case 'info': + this.outputChannel.info(data.message); + break; + case 'warning': + this.outputChannel.warn(data.message); + break; + case 'error': + this.outputChannel.error(data.message); + break; + case 'debug': + this.outputChannel.debug(data.message); + break; + default: + this.outputChannel.trace(data.message); + } + }), + connection.onNotification('telemetry', (data: NativePythonTelemetry) => + sendNativeTelemetry(data, this.initialRefreshMetrics), + ), + connection.onClose(() => { + disposables.forEach((d) => d.dispose()); + }), + ); + + connection.listen(); + this._register(Disposable.from(...disposables)); + return connection; + } + + private doRefresh( + options?: NativePythonEnvironmentKind | Uri[], + ): { completed: Promise; discovered: Event } { + const disposable = this._register(new DisposableStore()); + const discovered = disposable.add(new EventEmitter()); + const completed = createDeferred(); + const pendingPromises: Promise[] = []; + const stopWatch = new StopWatch(); + + const notifyUponCompletion = () => { + const initialCount = pendingPromises.length; + Promise.all(pendingPromises) + .then(() => { + if (initialCount === pendingPromises.length) { + completed.resolve(); + } else { + setTimeout(notifyUponCompletion, 0); + } + }) + .catch(noop); + }; + const trackPromiseAndNotifyOnCompletion = (promise: Promise) => { + pendingPromises.push(promise); + notifyUponCompletion(); + }; + + // Assumption is server will ensure there's only one refresh at a time. + // Perhaps we should have a request Id or the like to map the results back to the `refresh` request. + disposable.add( + this.connection.onNotification('environment', (data: NativeEnvInfo) => { + this.outputChannel.info(`Discovered env: ${data.executable || data.prefix}`); + // We know that in the Python extension if either Version of Prefix is not provided by locator + // Then we end up resolving the information. + // Lets do that here, + // This is a hack, as the other part of the code that resolves the version information + // doesn't work as expected, as its still a WIP. + if (data.executable && (!data.version || !data.prefix)) { + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + const promise = this.connection + .sendRequest('resolve', { + executable: data.executable, + }) + .then((environment) => { + this.outputChannel.info(`Resolved ${environment.executable}`); + discovered.fire(environment); + }) + .catch((ex) => this.outputChannel.error(`Error in Resolving ${JSON.stringify(data)}`, ex)); + trackPromiseAndNotifyOnCompletion(promise); + } else { + discovered.fire(data); + } + }), + ); + disposable.add( + this.connection.onNotification('manager', (data: NativeEnvManagerInfo) => { + this.outputChannel.info(`Discovered manager: (${data.tool}) ${data.executable}`); + discovered.fire(data); + }), + ); + + type RefreshOptions = { + searchKind?: NativePythonEnvironmentKind; + searchPaths?: string[]; + }; + + const refreshOptions: RefreshOptions = {}; + if (options && Array.isArray(options) && options.length > 0) { + refreshOptions.searchPaths = options.map((item) => item.fsPath); + } else if (options && typeof options === 'string') { + refreshOptions.searchKind = options; + } + trackPromiseAndNotifyOnCompletion( + this.configure().then(() => + this.connection + .sendRequest<{ duration: number }>('refresh', refreshOptions) + .then(({ duration }) => { + this.outputChannel.info(`Refresh completed in ${duration}ms`); + this.initialRefreshMetrics.timeToRefresh = stopWatch.elapsedTime; + }) + .catch((ex) => this.outputChannel.error('Refresh error', ex)), + ), + ); + + completed.promise.finally(() => disposable.dispose()); + return { + completed: completed.promise, + discovered: discovered.event, + }; + } + + private lastConfiguration?: ConfigurationOptions; + + /** + * Configuration request, this must always be invoked before any other request. + * Must be invoked when ever there are changes to any data related to the configuration details. + */ + private async configure() { + const options: ConfigurationOptions = { + workspaceDirectories: getWorkspaceFolderPaths(), + // We do not want to mix this with `search_paths` + environmentDirectories: getCustomVirtualEnvDirs(), + condaExecutable: getPythonSettingAndUntildify(CONDAPATH_SETTING_KEY), + poetryExecutable: getPythonSettingAndUntildify('poetryPath'), + cacheDirectory: this.cacheDirectory?.fsPath, + }; + // No need to send a configuration request, is there are no changes. + if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) { + return; + } + try { + const stopWatch = new StopWatch(); + this.lastConfiguration = options; + await this.connection.sendRequest('configure', options); + this.initialRefreshMetrics.timeToConfigure = stopWatch.elapsedTime; + } catch (ex) { + this.outputChannel.error('Refresh error', ex); + } + } + + async getCondaInfo(): Promise { + return this.connection.sendRequest('condaInfo'); + } + + private async handleSpawnError(errorMessage: string): Promise { + // Check if user has chosen to not see this error again + if (this.suppressErrorNotification.get()) { + return; + } + + // Check for Windows runtime DLL issues + if (isWindows() && errorMessage.toLowerCase().includes('vcruntime')) { + this.outputChannel.error(PythonLocator.windowsRuntimeMissing); + } else if (isWindows()) { + this.outputChannel.error(PythonLocator.windowsStartupFailed); + } + + // Show notification to user + const selection = await showWarningMessage( + PythonLocator.startupFailedNotification, + Common.openOutputPanel, + Common.doNotShowAgain, + ); + + if (selection === Common.openOutputPanel) { + await executeCommand(Commands.ViewOutput); + } else if (selection === Common.doNotShowAgain) { + await this.suppressErrorNotification.set(true); + } + } +} + +type ConfigurationOptions = { + workspaceDirectories: string[]; + /** + * Place where virtual envs and the like are stored + * Should not contain workspace folders. + */ + environmentDirectories: string[]; + condaExecutable: string | undefined; + poetryExecutable: string | undefined; + cacheDirectory?: string; +}; +/** + * Gets all custom virtual environment locations to look for environments. + */ +function getCustomVirtualEnvDirs(): string[] { + const venvDirs: string[] = []; + const venvPath = getPythonSettingAndUntildify(VENVPATH_SETTING_KEY); + if (venvPath) { + venvDirs.push(untildify(venvPath)); + } + const venvFolders = getPythonSettingAndUntildify(VENVFOLDERS_SETTING_KEY) ?? []; + const homeDir = getUserHomeDir(); + if (homeDir) { + venvFolders + .map((item) => (item.startsWith(homeDir) ? item : path.join(homeDir, item))) + .forEach((d) => venvDirs.push(d)); + venvFolders.forEach((item) => venvDirs.push(untildify(item))); + } + return Array.from(new Set(venvDirs)); +} + +function getPythonSettingAndUntildify(name: string, scope?: Uri): T | undefined { + const value = getConfiguration('python', scope).get(name); + if (typeof value === 'string') { + return value ? ((untildify(value as string) as unknown) as T) : undefined; + } + return value; +} + +let _finder: NativePythonFinder | undefined; +export function getNativePythonFinder(context?: IExtensionContext): NativePythonFinder { + if (!isTrusted()) { + return { + async *refresh() { + traceError('Python discovery not supported in untrusted workspace'); + yield* []; + }, + async resolve() { + traceError('Python discovery not supported in untrusted workspace'); + return {}; + }, + async getCondaInfo() { + traceError('Python discovery not supported in untrusted workspace'); + return ({} as unknown) as NativeCondaInfo; + }, + dispose() { + // do nothing + }, + }; + } + if (!_finder) { + const cacheDirectory = context ? getCacheDirectory(context) : undefined; + _finder = new NativePythonFinderImpl(cacheDirectory, context); + if (context) { + context.subscriptions.push(_finder); + } + } + return _finder; +} + +export function getCacheDirectory(context: IExtensionContext): Uri { + return Uri.joinPath(context.globalStorageUri, 'pythonLocator'); +} + +export async function clearCacheDirectory(context: IExtensionContext): Promise { + const cacheDirectory = getCacheDirectory(context); + await fs.emptyDir(cacheDirectory.fsPath).catch(noop); +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts new file mode 100644 index 000000000000..703fdfca01c3 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceError } from '../../../../logging'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; + +export type NativePythonTelemetry = MissingCondaEnvironments | MissingPoetryEnvironments | RefreshPerformance; + +export type MissingCondaEnvironments = { + event: 'MissingCondaEnvironments'; + data: { + missingCondaEnvironments: { + missing: number; + envDirsNotFound?: number; + userProvidedCondaExe?: boolean; + rootPrefixNotFound?: boolean; + condaPrefixNotFound?: boolean; + condaManagerNotFound?: boolean; + sysRcNotFound?: boolean; + userRcNotFound?: boolean; + otherRcNotFound?: boolean; + missingEnvDirsFromSysRc?: number; + missingEnvDirsFromUserRc?: number; + missingEnvDirsFromOtherRc?: number; + missingFromSysRcEnvDirs?: number; + missingFromUserRcEnvDirs?: number; + missingFromOtherRcEnvDirs?: number; + }; + }; +}; + +export type MissingPoetryEnvironments = { + event: 'MissingPoetryEnvironments'; + data: { + missingPoetryEnvironments: { + missing: number; + missingInPath: number; + userProvidedPoetryExe?: boolean; + poetryExeNotFound?: boolean; + globalConfigNotFound?: boolean; + cacheDirNotFound?: boolean; + cacheDirIsDifferent?: boolean; + virtualenvsPathNotFound?: boolean; + virtualenvsPathIsDifferent?: boolean; + inProjectIsDifferent?: boolean; + }; + }; +}; + +export type RefreshPerformance = { + event: 'RefreshPerformance'; + data: { + refreshPerformance: { + total: number; + breakdown: { + Locators: number; + Path: number; + GlobalVirtualEnvs: number; + Workspaces: number; + }; + locators: { + Conda?: number; + Homebrew?: number; + LinuxGlobalPython?: number; + MacCmdLineTools?: number; + MacPythonOrg?: number; + MacXCode?: number; + PipEnv?: number; + PixiEnv?: number; + Poetry?: number; + PyEnv?: number; + Venv?: number; + VirtualEnv?: number; + VirtualEnvWrapper?: number; + WindowsRegistry?: number; + WindowsStore?: number; + }; + }; + }; +}; + +let refreshTelemetrySent = false; + +export function sendNativeTelemetry( + data: NativePythonTelemetry, + initialRefreshMetrics: { + timeToSpawn: number; + timeToConfigure: number; + timeToRefresh: number; + }, +): void { + switch (data.event) { + case 'MissingCondaEnvironments': { + sendTelemetryEvent( + EventName.NATIVE_FINDER_MISSING_CONDA_ENVS, + undefined, + data.data.missingCondaEnvironments, + ); + break; + } + case 'MissingPoetryEnvironments': { + sendTelemetryEvent( + EventName.NATIVE_FINDER_MISSING_POETRY_ENVS, + undefined, + data.data.missingPoetryEnvironments, + ); + break; + } + case 'RefreshPerformance': { + if (refreshTelemetrySent) { + break; + } + refreshTelemetrySent = true; + sendTelemetryEvent(EventName.NATIVE_FINDER_PERF, { + duration: data.data.refreshPerformance.total, + totalDuration: data.data.refreshPerformance.total, + breakdownGlobalVirtualEnvs: data.data.refreshPerformance.breakdown.GlobalVirtualEnvs, + breakdownLocators: data.data.refreshPerformance.breakdown.Locators, + breakdownPath: data.data.refreshPerformance.breakdown.Path, + breakdownWorkspaces: data.data.refreshPerformance.breakdown.Workspaces, + locatorConda: data.data.refreshPerformance.locators.Conda || 0, + locatorHomebrew: data.data.refreshPerformance.locators.Homebrew || 0, + locatorLinuxGlobalPython: data.data.refreshPerformance.locators.LinuxGlobalPython || 0, + locatorMacCmdLineTools: data.data.refreshPerformance.locators.MacCmdLineTools || 0, + locatorMacPythonOrg: data.data.refreshPerformance.locators.MacPythonOrg || 0, + locatorMacXCode: data.data.refreshPerformance.locators.MacXCode || 0, + locatorPipEnv: data.data.refreshPerformance.locators.PipEnv || 0, + locatorPixiEnv: data.data.refreshPerformance.locators.PixiEnv || 0, + locatorPoetry: data.data.refreshPerformance.locators.Poetry || 0, + locatorPyEnv: data.data.refreshPerformance.locators.PyEnv || 0, + locatorVenv: data.data.refreshPerformance.locators.Venv || 0, + locatorVirtualEnv: data.data.refreshPerformance.locators.VirtualEnv || 0, + locatorVirtualEnvWrapper: data.data.refreshPerformance.locators.VirtualEnvWrapper || 0, + locatorWindowsRegistry: data.data.refreshPerformance.locators.WindowsRegistry || 0, + locatorWindowsStore: data.data.refreshPerformance.locators.WindowsStore || 0, + timeToSpawn: initialRefreshMetrics.timeToSpawn, + timeToConfigure: initialRefreshMetrics.timeToConfigure, + timeToRefresh: initialRefreshMetrics.timeToRefresh, + }); + break; + } + default: { + traceError(`Unhandled Telemetry Event type ${JSON.stringify(data)}`); + } + } +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts new file mode 100644 index 000000000000..716bdd444633 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { LogOutputChannel } from 'vscode'; +import { PythonEnvKind } from '../../info'; +import { traceError } from '../../../../logging'; + +export enum NativePythonEnvironmentKind { + Conda = 'Conda', + Pixi = 'Pixi', + Homebrew = 'Homebrew', + Pyenv = 'Pyenv', + GlobalPaths = 'GlobalPaths', + PyenvVirtualEnv = 'PyenvVirtualEnv', + Pipenv = 'Pipenv', + Poetry = 'Poetry', + MacPythonOrg = 'MacPythonOrg', + MacCommandLineTools = 'MacCommandLineTools', + LinuxGlobal = 'LinuxGlobal', + MacXCode = 'MacXCode', + Venv = 'Venv', + VirtualEnv = 'VirtualEnv', + VirtualEnvWrapper = 'VirtualEnvWrapper', + WindowsStore = 'WindowsStore', + WindowsRegistry = 'WindowsRegistry', + VenvUv = 'Uv', +} + +const mapping = new Map([ + [NativePythonEnvironmentKind.Conda, PythonEnvKind.Conda], + [NativePythonEnvironmentKind.Pixi, PythonEnvKind.Pixi], + [NativePythonEnvironmentKind.GlobalPaths, PythonEnvKind.OtherGlobal], + [NativePythonEnvironmentKind.Pyenv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.PyenvVirtualEnv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.Pipenv, PythonEnvKind.Pipenv], + [NativePythonEnvironmentKind.Poetry, PythonEnvKind.Poetry], + [NativePythonEnvironmentKind.VirtualEnv, PythonEnvKind.VirtualEnv], + [NativePythonEnvironmentKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnvWrapper], + [NativePythonEnvironmentKind.Venv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.VenvUv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.WindowsRegistry, PythonEnvKind.System], + [NativePythonEnvironmentKind.WindowsStore, PythonEnvKind.MicrosoftStore], + [NativePythonEnvironmentKind.Homebrew, PythonEnvKind.System], + [NativePythonEnvironmentKind.LinuxGlobal, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacCommandLineTools, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacPythonOrg, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacXCode, PythonEnvKind.System], +]); + +export function categoryToKind(category?: NativePythonEnvironmentKind, logger?: LogOutputChannel): PythonEnvKind { + if (!category) { + return PythonEnvKind.Unknown; + } + const kind = mapping.get(category); + if (kind) { + return kind; + } + + if (logger) { + logger.error(`Unknown Python Environment category '${category}' from Native Locator.`); + } else { + traceError(`Unknown Python Environment category '${category}' from Native Locator.`); + } + return PythonEnvKind.Unknown; +} diff --git a/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts b/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts new file mode 100644 index 000000000000..378a0d6c521e --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Event, EventEmitter, GlobPattern, RelativePattern, Uri, WorkspaceFolder } from 'vscode'; +import { createFileSystemWatcher, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { isWindows } from '../../../../common/utils/platform'; +import { arePathsSame } from '../../../common/externalDependencies'; +import { FileChangeType } from '../../../../common/platform/fileSystemWatcher'; + +export interface PythonWorkspaceEnvEvent { + type: FileChangeType; + workspaceFolder: WorkspaceFolder; + executable: string; +} + +export interface PythonGlobalEnvEvent { + type: FileChangeType; + uri: Uri; +} + +export interface PythonWatcher extends Disposable { + watchWorkspace(wf: WorkspaceFolder): void; + unwatchWorkspace(wf: WorkspaceFolder): void; + onDidWorkspaceEnvChanged: Event; + + watchPath(uri: Uri, pattern?: string): void; + unwatchPath(uri: Uri): void; + onDidGlobalEnvChanged: Event; +} + +/* + * The pattern to search for python executables in the workspace. + * project + * ├── python or python.exe <--- This is what we are looking for. + * ├── .conda + * │ └── python or python.exe <--- This is what we are looking for. + * └── .venv + * │ └── Scripts or bin + * │ └── python or python.exe <--- This is what we are looking for. + */ +const WORKSPACE_PATTERN = isWindows() ? '**/python.exe' : '**/python'; + +class PythonWatcherImpl implements PythonWatcher { + private disposables: Disposable[] = []; + + private readonly _onDidWorkspaceEnvChanged = new EventEmitter(); + + private readonly _onDidGlobalEnvChanged = new EventEmitter(); + + private readonly _disposeMap: Map = new Map(); + + constructor() { + this.disposables.push(this._onDidWorkspaceEnvChanged, this._onDidGlobalEnvChanged); + } + + onDidGlobalEnvChanged: Event = this._onDidGlobalEnvChanged.event; + + onDidWorkspaceEnvChanged: Event = this._onDidWorkspaceEnvChanged.event; + + watchWorkspace(wf: WorkspaceFolder): void { + if (this._disposeMap.has(wf.uri.fsPath)) { + const disposer = this._disposeMap.get(wf.uri.fsPath); + disposer?.dispose(); + } + + const disposables: Disposable[] = []; + const watcher = createFileSystemWatcher(new RelativePattern(wf, WORKSPACE_PATTERN)); + disposables.push( + watcher, + watcher.onDidChange((uri) => { + this.fireWorkspaceEvent(FileChangeType.Changed, wf, uri); + }), + watcher.onDidCreate((uri) => { + this.fireWorkspaceEvent(FileChangeType.Created, wf, uri); + }), + watcher.onDidDelete((uri) => { + this.fireWorkspaceEvent(FileChangeType.Deleted, wf, uri); + }), + ); + + const disposable = { + dispose: () => { + disposables.forEach((d) => d.dispose()); + this._disposeMap.delete(wf.uri.fsPath); + }, + }; + this._disposeMap.set(wf.uri.fsPath, disposable); + } + + unwatchWorkspace(wf: WorkspaceFolder): void { + const disposable = this._disposeMap.get(wf.uri.fsPath); + disposable?.dispose(); + } + + private fireWorkspaceEvent(type: FileChangeType, wf: WorkspaceFolder, uri: Uri) { + const uriWorkspace = getWorkspaceFolder(uri); + if (uriWorkspace && arePathsSame(uriWorkspace.uri.fsPath, wf.uri.fsPath)) { + this._onDidWorkspaceEnvChanged.fire({ type, workspaceFolder: wf, executable: uri.fsPath }); + } + } + + watchPath(uri: Uri, pattern?: string): void { + if (this._disposeMap.has(uri.fsPath)) { + const disposer = this._disposeMap.get(uri.fsPath); + disposer?.dispose(); + } + + const glob: GlobPattern = pattern ? new RelativePattern(uri, pattern) : uri.fsPath; + const disposables: Disposable[] = []; + const watcher = createFileSystemWatcher(glob); + disposables.push( + watcher, + watcher.onDidChange(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Changed, uri }); + }), + watcher.onDidCreate(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Created, uri }); + }), + watcher.onDidDelete(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Deleted, uri }); + }), + ); + + const disposable = { + dispose: () => { + disposables.forEach((d) => d.dispose()); + this._disposeMap.delete(uri.fsPath); + }, + }; + this._disposeMap.set(uri.fsPath, disposable); + } + + unwatchPath(uri: Uri): void { + const disposable = this._disposeMap.get(uri.fsPath); + disposable?.dispose(); + } + + dispose() { + this.disposables.forEach((d) => d.dispose()); + this._disposeMap.forEach((d) => d.dispose()); + } +} + +export function createPythonWatcher(): PythonWatcher { + return new PythonWatcherImpl(); +} diff --git a/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts new file mode 100644 index 000000000000..8b56b4c7b8c1 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposable } from '../../../../common/types'; +import { createDeferred, Deferred } from '../../../../common/utils/async'; +import { Disposables } from '../../../../common/utils/resourceLifecycle'; +import { traceError, traceWarn } from '../../../../logging'; +import { arePathsSame, isVirtualWorkspace } from '../../../common/externalDependencies'; +import { getEnvPath } from '../../info/env'; +import { BasicEnvInfo, IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../locator'; + +/** + * A base locator class that manages the lifecycle of resources. + * + * The resources are not initialized until needed. + * + * It is critical that each subclass properly add its resources + * to the list: + * + * this.disposables.push(someResource); + * + * Otherwise it will leak (and we have no leak detection). + */ +export abstract class LazyResourceBasedLocator extends Locator implements IDisposable { + protected readonly disposables = new Disposables(); + + // This will be set only once we have to create necessary resources + // and resolves once those resources are ready. + private resourcesReady?: Deferred; + + private watchersReady?: Deferred; + + /** + * This can be used to initialize resources when subclasses are created. + */ + protected async activate(): Promise { + await this.ensureResourcesReady(); + // There is not need to wait for the watchers to get started. + try { + this.ensureWatchersReady(); + } catch (ex) { + traceWarn(`Failed to ensure watchers are ready for locator ${this.constructor.name}`, ex); + } + } + + public async dispose(): Promise { + await this.disposables.dispose(); + } + + public async *iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + await this.activate(); + const iterator = this.doIterEnvs(query); + if (query?.envPath) { + let result = await iterator.next(); + while (!result.done) { + const currEnv = result.value; + const { path } = getEnvPath(currEnv.executablePath, currEnv.envPath); + if (arePathsSame(path, query.envPath)) { + yield currEnv; + break; + } + result = await iterator.next(); + } + } else { + yield* iterator; + } + } + + /** + * The subclass implementation of iterEnvs(). + */ + protected abstract doIterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator; + + /** + * This is where subclasses get their resources ready. + * + * It is only called once resources are needed. + * + * Each subclass is responsible to add its resources to the list + * (otherwise it leaks): + * + * this.disposables.push(someResource); + * + * Not all locators have resources other than watchers so a default + * implementation is provided. + */ + // eslint-disable-next-line class-methods-use-this + protected async initResources(): Promise { + // No resources! + } + + /** + * This is where subclasses get their watchers ready. + * + * It is only called with the first `iterEnvs()` call, + * after `initResources()` has been called. + * + * Each subclass is responsible to add its resources to the list + * (otherwise it leaks): + * + * this.disposables.push(someResource); + * + * Not all locators have watchers to init so a default + * implementation is provided. + */ + // eslint-disable-next-line class-methods-use-this + protected async initWatchers(): Promise { + // No watchers! + } + + protected async ensureResourcesReady(): Promise { + if (this.resourcesReady !== undefined) { + await this.resourcesReady.promise; + return; + } + this.resourcesReady = createDeferred(); + await this.initResources().catch((ex) => { + traceError(ex); + this.resourcesReady?.reject(ex); + }); + this.resourcesReady.resolve(); + } + + private async ensureWatchersReady(): Promise { + if (this.watchersReady !== undefined) { + await this.watchersReady.promise; + return; + } + this.watchersReady = createDeferred(); + + // Don't create any file watchers in a virtual workspace. + if (!isVirtualWorkspace()) { + await this.initWatchers().catch((ex) => { + traceError(ex); + this.watchersReady?.reject(ex); + }); + } + this.watchersReady.resolve(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts new file mode 100644 index 000000000000..456e8adfa9a4 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event } from 'vscode'; +import { isTestExecution } from '../../../../common/constants'; +import { traceVerbose } from '../../../../logging'; +import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; +import { areEnvsDeepEqual, areSameEnv, getEnvPath } from '../../info/env'; +import { + BasicPythonEnvCollectionChangedEvent, + PythonEnvCollectionChangedEvent, + PythonEnvsWatcher, +} from '../../watcher'; +import { getCondaInterpreterPath } from '../../../common/environmentManagers/conda'; + +export interface IEnvsCollectionCache { + /** + * Return all environment info currently in memory for this session. + */ + getAllEnvs(): PythonEnvInfo[]; + + /** + * Updates environment in cache using the value provided. + * If no new value is provided, remove the existing value from cache. + */ + updateEnv(oldValue: PythonEnvInfo, newValue: PythonEnvInfo | undefined): void; + + /** + * Fires with details if the cache changes. + */ + onChanged: Event; + + /** + * Adds environment to cache. + */ + addEnv(env: PythonEnvInfo, hasLatestInfo?: boolean): void; + + /** + * Return cached environment information for a given path if it exists and + * is up to date, otherwise return `undefined`. + * + * @param path - Python executable path or path to environment + */ + getLatestInfo(path: string): Promise; + + /** + * Writes the content of the in-memory cache to persistent storage. It is assumed + * all envs have upto date info when this is called. + */ + flush(): Promise; + + /** + * Removes invalid envs from cache. Note this does not check for outdated info when + * validating cache. + * @param envs Carries list of envs for the latest refresh. + * @param isCompleteList Carries whether the list of envs is complete or not. + */ + validateCache(envs?: PythonEnvInfo[], isCompleteList?: boolean): Promise; +} + +interface IPersistentStorage { + get(): PythonEnvInfo[]; + store(envs: PythonEnvInfo[]): Promise; +} + +/** + * Environment info cache using persistent storage to save and retrieve pre-cached env info. + */ +export class PythonEnvInfoCache extends PythonEnvsWatcher + implements IEnvsCollectionCache { + private envs: PythonEnvInfo[] = []; + + /** + * Carries the list of envs which have been validated to have latest info. + */ + private validatedEnvs = new Set(); + + /** + * Carries the list of envs which have been flushed to persistent storage. + * It signifies that the env info is likely up-to-date. + */ + private flushedEnvs = new Set(); + + constructor(private readonly persistentStorage: IPersistentStorage) { + super(); + } + + public async validateCache(envs?: PythonEnvInfo[], isCompleteList?: boolean): Promise { + /** + * We do check if an env has updated as we already run discovery in background + * which means env cache will have up-to-date envs eventually. This also means + * we avoid the cost of running lstat. So simply remove envs which are no longer + * valid. + */ + const areEnvsValid = await Promise.all( + this.envs.map(async (cachedEnv) => { + const { path } = getEnvPath(cachedEnv.executable.filename, cachedEnv.location); + if (await pathExists(path)) { + if (envs && isCompleteList) { + /** + * Only consider a cached env to be valid if it's relevant. That means: + * * It is relevant for some other workspace folder which is not opened currently. + * * It is either reported in the latest complete discovery for this session. + * * It is provided by the consumer themselves. + */ + if (cachedEnv.searchLocation) { + return true; + } + if (envs.some((env) => cachedEnv.id === env.id)) { + return true; + } + if (Array.from(this.validatedEnvs.keys()).some((envId) => cachedEnv.id === envId)) { + // These envs are provided by the consumer themselves, consider them valid. + return true; + } + } else { + return true; + } + } + return false; + }), + ); + const invalidIndexes = areEnvsValid + .map((isValid, index) => (isValid ? -1 : index)) + .filter((i) => i !== -1) + .reverse(); // Reversed so indexes do not change when deleting + invalidIndexes.forEach((index) => { + const env = this.envs.splice(index, 1)[0]; + traceVerbose(`Removing invalid env from cache ${env.id}`); + this.fire({ old: env, new: undefined }); + }); + if (envs) { + // See if any env has updated after the last refresh and fire events. + envs.forEach((env) => { + const cachedEnv = this.envs.find((e) => e.id === env.id); + if (cachedEnv && !areEnvsDeepEqual(cachedEnv, env)) { + this.updateEnv(cachedEnv, env, true); + } + }); + } + } + + public getAllEnvs(): PythonEnvInfo[] { + return this.envs; + } + + public addEnv(env: PythonEnvInfo, hasLatestInfo?: boolean): void { + const found = this.envs.find((e) => areSameEnv(e, env)); + if (!found) { + this.envs.push(env); + this.fire({ new: env }); + } else if (hasLatestInfo && !this.validatedEnvs.has(env.id!)) { + // Update cache if we have latest info and the env is not already validated. + this.updateEnv(found, env, true); + } + if (hasLatestInfo) { + traceVerbose(`Flushing env to cache ${env.id}`); + this.validatedEnvs.add(env.id!); + this.flush(env).ignoreErrors(); // If we have latest info, flush it so it can be saved. + } + } + + public updateEnv(oldValue: PythonEnvInfo, newValue: PythonEnvInfo | undefined, forceUpdate = false): void { + if (this.flushedEnvs.has(oldValue.id!) && !forceUpdate) { + // We have already flushed this env to persistent storage, so it likely has upto date info. + // If we have latest info, then we do not need to update the cache. + return; + } + const index = this.envs.findIndex((e) => areSameEnv(e, oldValue)); + if (index !== -1) { + if (newValue === undefined) { + this.envs.splice(index, 1); + } else { + this.envs[index] = newValue; + } + this.fire({ old: oldValue, new: newValue }); + } + } + + public async getLatestInfo(path: string): Promise { + // `path` can either be path to environment or executable path + const env = this.envs.find((e) => arePathsSame(e.location, path)) ?? this.envs.find((e) => areSameEnv(e, path)); + if ( + env?.kind === PythonEnvKind.Conda && + getEnvPath(env.executable.filename, env.location).pathType === 'envFolderPath' + ) { + if (await pathExists(getCondaInterpreterPath(env.location))) { + // This is a conda env without python in cache which actually now has a valid python, so return + // `undefined` and delete value from cache as cached value is not the latest anymore. + this.validatedEnvs.delete(env.id!); + return undefined; + } + // Do not attempt to validate these envs as they lack an executable, and consider them as validated by default. + this.validatedEnvs.add(env.id!); + return env; + } + if (env) { + if (this.validatedEnvs.has(env.id!)) { + traceVerbose(`Found cached env for ${path}`); + return env; + } + if (await this.validateInfo(env)) { + traceVerbose(`Needed to validate ${path} with latest info`); + this.validatedEnvs.add(env.id!); + return env; + } + } + traceVerbose(`No cached env found for ${path}`); + return undefined; + } + + public clearAndReloadFromStorage(): void { + this.envs = this.persistentStorage.get(); + this.markAllEnvsAsFlushed(); + } + + public async flush(env?: PythonEnvInfo): Promise { + if (env) { + // Flush only the given env. + const envs = this.persistentStorage.get(); + const index = envs.findIndex((e) => e.id === env.id); + envs[index] = env; + this.flushedEnvs.add(env.id!); + await this.persistentStorage.store(envs); + return; + } + traceVerbose('Environments added to cache', JSON.stringify(this.envs)); + this.markAllEnvsAsFlushed(); + await this.persistentStorage.store(this.envs); + } + + private markAllEnvsAsFlushed(): void { + this.envs.forEach((e) => { + this.flushedEnvs.add(e.id!); + }); + } + + /** + * Ensure environment has complete and latest information. + */ + private async validateInfo(env: PythonEnvInfo) { + // Make sure any previously flushed information is upto date by ensuring environment did not change. + if (!this.flushedEnvs.has(env.id!)) { + // Any environment with complete information is flushed, so this env does not contain complete info. + return false; + } + if (env.version.micro === -1 || env.version.major === -1 || env.version.minor === -1) { + // Env should not contain incomplete versions. + return false; + } + const { ctime, mtime } = await getFileInfo(env.executable.filename); + if (ctime !== -1 && mtime !== -1 && ctime === env.executable.ctime && mtime === env.executable.mtime) { + return true; + } + env.executable.ctime = ctime; + env.executable.mtime = mtime; + return false; + } +} + +/** + * Build a cache of PythonEnvInfo that is ready to use. + */ +export async function createCollectionCache(storage: IPersistentStorage): Promise { + const cache = new PythonEnvInfoCache(storage); + cache.clearAndReloadFromStorage(); + await validateCache(cache); + return cache; +} + +async function validateCache(cache: PythonEnvInfoCache) { + if (isTestExecution()) { + // For purposes for test execution, block on validation so that we can determinally know when it finishes. + return cache.validateCache(); + } + // Validate in background so it doesn't block on returning the API object. + return cache.validateCache().ignoreErrors(); +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts new file mode 100644 index 000000000000..25ceb267da85 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, EventEmitter } from 'vscode'; +import '../../../../common/extensions'; +import { createDeferred, Deferred } from '../../../../common/utils/async'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { normalizePath } from '../../../common/externalDependencies'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; +import { getEnvPath } from '../../info/env'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + IResolvingLocator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../../locator'; +import { getQueryFilter } from '../../locatorUtils'; +import { PythonEnvCollectionChangedEvent, PythonEnvsWatcher } from '../../watcher'; +import { IEnvsCollectionCache } from './envsCollectionCache'; + +/** + * A service which maintains the collection of known environments. + */ +export class EnvsCollectionService extends PythonEnvsWatcher implements IDiscoveryAPI { + /** Keeps track of ongoing refreshes for various queries. */ + private refreshesPerQuery = new Map>(); + + /** Keeps track of scheduled refreshes other than the ongoing one for various queries. */ + private scheduledRefreshesPerQuery = new Map>(); + + /** Keeps track of promises which resolves when a stage has been reached */ + private progressPromises = new Map>(); + + /** Keeps track of whether a refresh has been triggered for various queries. */ + private hasRefreshFinishedForQuery = new Map(); + + private readonly progress = new EventEmitter(); + + public refreshState = ProgressReportStage.discoveryFinished; + + public get onProgress(): Event { + return this.progress.event; + } + + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + const stage = options?.stage ?? ProgressReportStage.discoveryFinished; + return this.progressPromises.get(stage)?.promise; + } + + constructor( + private readonly cache: IEnvsCollectionCache, + private readonly locator: IResolvingLocator, + private readonly usingNativeLocator: boolean, + ) { + super(); + this.locator.onChanged((event) => { + const query: PythonLocatorQuery | undefined = event.providerId + ? { providerId: event.providerId, envPath: event.envPath } + : undefined; // We can also form a query based on the event, but skip that for simplicity. + let scheduledRefresh = this.scheduledRefreshesPerQuery.get(query); + // If there is no refresh scheduled for the query, start a new one. + if (!scheduledRefresh) { + scheduledRefresh = this.scheduleNewRefresh(query); + } + scheduledRefresh.then(() => { + // Once refresh of cache is complete, notify changes. + this.fire(event); + }); + }); + this.cache.onChanged((e) => { + this.fire(e); + }); + this.onProgress((event) => { + this.refreshState = event.stage; + // Resolve progress promise indicating the stage has been reached. + this.progressPromises.get(event.stage)?.resolve(); + this.progressPromises.delete(event.stage); + }); + } + + public async resolveEnv(path: string): Promise { + path = normalizePath(path); + // Note cache may have incomplete info when a refresh is happening. + // This API is supposed to return complete info by definition, so + // only use cache if it has complete info on an environment. + const cachedEnv = await this.cache.getLatestInfo(path); + if (cachedEnv) { + return cachedEnv; + } + const resolved = await this.locator.resolveEnv(path).catch((ex) => { + traceError(`Failed to resolve ${path}`, ex); + return undefined; + }); + traceVerbose(`Resolved ${path} using downstream locator`); + if (resolved) { + this.cache.addEnv(resolved, true); + } + return resolved; + } + + public getEnvs(query?: PythonLocatorQuery): PythonEnvInfo[] { + const cachedEnvs = this.cache.getAllEnvs(); + return query ? cachedEnvs.filter(getQueryFilter(query)) : cachedEnvs; + } + + public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise { + let refreshPromise = this.getRefreshPromiseForQuery(query); + if (!refreshPromise) { + if (options?.ifNotTriggerredAlready && this.hasRefreshFinished(query)) { + // Do not trigger another refresh if a refresh has previously finished. + return Promise.resolve(); + } + const stopWatch = new StopWatch(); + traceInfo(`Starting Environment refresh`); + refreshPromise = this.startRefresh(query).then(() => { + this.sendTelemetry(query, stopWatch); + traceInfo(`Environment refresh took ${stopWatch.elapsedTime} milliseconds`); + }); + } + return refreshPromise; + } + + private startRefresh(query: PythonLocatorQuery | undefined): Promise { + this.createProgressStates(query); + const promise = this.addEnvsToCacheForQuery(query); + return promise + .then(async () => { + this.resolveProgressStates(query); + }) + .catch((ex) => { + this.rejectProgressStates(query, ex); + }); + } + + private async addEnvsToCacheForQuery(query: PythonLocatorQuery | undefined) { + const iterator = this.locator.iterEnvs(query); + const seen: PythonEnvInfo[] = []; + const state = { + done: false, + pending: 0, + }; + const updatesDone = createDeferred(); + const stopWatch = new StopWatch(); + if (iterator.onUpdated !== undefined) { + const listener = iterator.onUpdated(async (event) => { + if (isProgressEvent(event)) { + switch (event.stage) { + case ProgressReportStage.discoveryFinished: + state.done = true; + listener.dispose(); + traceInfo(`Environments refresh finished (event): ${stopWatch.elapsedTime} milliseconds`); + break; + case ProgressReportStage.allPathsDiscovered: + if (!query) { + traceInfo( + `Environments refresh paths discovered (event): ${stopWatch.elapsedTime} milliseconds`, + ); + // Only mark as all paths discovered when querying for all envs. + this.progress.fire(event); + } + break; + default: + this.progress.fire(event); + } + } else if (event.index !== undefined) { + state.pending += 1; + this.cache.updateEnv(seen[event.index], event.update); + if (event.update) { + seen[event.index] = event.update; + } + state.pending -= 1; + } + if (state.done && state.pending === 0) { + updatesDone.resolve(); + } + }); + } else { + this.progress.fire({ stage: ProgressReportStage.discoveryStarted }); + updatesDone.resolve(); + } + + for await (const env of iterator) { + seen.push(env); + this.cache.addEnv(env); + } + traceInfo(`Environments refresh paths discovered: ${stopWatch.elapsedTime} milliseconds`); + await updatesDone.promise; + // If query for all envs is done, `seen` should contain the list of all envs. + await this.cache.validateCache(seen, query === undefined); + this.cache.flush().ignoreErrors(); + } + + /** + * See if we already have a refresh promise for the query going on and return it. + */ + private getRefreshPromiseForQuery(query?: PythonLocatorQuery) { + // Even if no refresh is running for this exact query, there might be other + // refreshes running for a superset of this query. For eg. the `undefined` query + // is a superset for every other query, only consider that for simplicity. + return this.refreshesPerQuery.get(query)?.promise ?? this.refreshesPerQuery.get(undefined)?.promise; + } + + private hasRefreshFinished(query?: PythonLocatorQuery) { + return this.hasRefreshFinishedForQuery.get(query) ?? this.hasRefreshFinishedForQuery.get(undefined); + } + + /** + * Ensure we trigger a fresh refresh for the query after the current refresh (if any) is done. + */ + private async scheduleNewRefresh(query?: PythonLocatorQuery): Promise { + const refreshPromise = this.getRefreshPromiseForQuery(query); + let nextRefreshPromise: Promise; + if (!refreshPromise) { + nextRefreshPromise = this.startRefresh(query); + } else { + nextRefreshPromise = refreshPromise.then(() => { + // No more scheduled refreshes for this query as we're about to start the scheduled one. + this.scheduledRefreshesPerQuery.delete(query); + this.startRefresh(query); + }); + this.scheduledRefreshesPerQuery.set(query, nextRefreshPromise); + } + return nextRefreshPromise; + } + + private createProgressStates(query: PythonLocatorQuery | undefined) { + this.refreshesPerQuery.set(query, createDeferred()); + Object.values(ProgressReportStage).forEach((stage) => { + this.progressPromises.set(stage, createDeferred()); + }); + if (ProgressReportStage.allPathsDiscovered && query) { + // Only mark as all paths discovered when querying for all envs. + this.progressPromises.delete(ProgressReportStage.allPathsDiscovered); + } + } + + private rejectProgressStates(query: PythonLocatorQuery | undefined, ex: Error) { + this.refreshesPerQuery.get(query)?.reject(ex); + this.refreshesPerQuery.delete(query); + Object.values(ProgressReportStage).forEach((stage) => { + this.progressPromises.get(stage)?.reject(ex); + this.progressPromises.delete(stage); + }); + } + + private resolveProgressStates(query: PythonLocatorQuery | undefined) { + this.refreshesPerQuery.get(query)?.resolve(); + this.refreshesPerQuery.delete(query); + // Refreshes per stage are resolved using progress events instead. + const isRefreshComplete = Array.from(this.refreshesPerQuery.values()).every((d) => d.completed); + if (isRefreshComplete) { + this.progress.fire({ stage: ProgressReportStage.discoveryFinished }); + } + } + + private sendTelemetry(query: PythonLocatorQuery | undefined, stopWatch: StopWatch) { + if (!query && !this.hasRefreshFinished(query)) { + const envs = this.cache.getAllEnvs(); + const environmentsWithoutPython = envs.filter( + (e) => getEnvPath(e.executable.filename, e.location).pathType === 'envFolderPath', + ).length; + const activeStateEnvs = envs.filter((e) => e.kind === PythonEnvKind.ActiveState).length; + const condaEnvs = envs.filter((e) => e.kind === PythonEnvKind.Conda).length; + const customEnvs = envs.filter((e) => e.kind === PythonEnvKind.Custom).length; + const hatchEnvs = envs.filter((e) => e.kind === PythonEnvKind.Hatch).length; + const microsoftStoreEnvs = envs.filter((e) => e.kind === PythonEnvKind.MicrosoftStore).length; + const otherGlobalEnvs = envs.filter((e) => e.kind === PythonEnvKind.OtherGlobal).length; + const otherVirtualEnvs = envs.filter((e) => e.kind === PythonEnvKind.OtherVirtual).length; + const pipEnvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Pipenv).length; + const poetryEnvs = envs.filter((e) => e.kind === PythonEnvKind.Poetry).length; + const pyenvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Pyenv).length; + const systemEnvs = envs.filter((e) => e.kind === PythonEnvKind.System).length; + const unknownEnvs = envs.filter((e) => e.kind === PythonEnvKind.Unknown).length; + const venvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Venv).length; + const virtualEnvEnvs = envs.filter((e) => e.kind === PythonEnvKind.VirtualEnv).length; + const virtualEnvWrapperEnvs = envs.filter((e) => e.kind === PythonEnvKind.VirtualEnvWrapper).length; + + // Intent is to capture time taken for discovery of all envs to complete the first time. + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_DISCOVERY, stopWatch.elapsedTime, { + interpreters: this.cache.getAllEnvs().length, + usingNativeLocator: this.usingNativeLocator, + environmentsWithoutPython, + activeStateEnvs, + condaEnvs, + customEnvs, + hatchEnvs, + microsoftStoreEnvs, + otherGlobalEnvs, + otherVirtualEnvs, + pipEnvEnvs, + poetryEnvs, + pyenvEnvs, + systemEnvs, + unknownEnvs, + venvEnvs, + virtualEnvEnvs, + virtualEnvWrapperEnvs, + }); + } + this.hasRefreshFinishedForQuery.set(query, true); + } +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts new file mode 100644 index 000000000000..c3a523b2d086 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep, isEqual, uniq } from 'lodash'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { traceVerbose } from '../../../../logging'; +import { isParentPath } from '../../../common/externalDependencies'; +import { PythonEnvKind } from '../../info'; +import { areSameEnv } from '../../info/env'; +import { getPrioritizedEnvKinds } from '../../info/envKind'; +import { + BasicEnvInfo, + ICompositeLocator, + ILocator, + IPythonEnvsIterator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../locator'; +import { PythonEnvsChangedEvent } from '../../watcher'; + +/** + * Combines duplicate environments received from the incoming locator into one and passes on unique environments + */ +export class PythonEnvsReducer implements ICompositeLocator { + public get onChanged(): Event { + return this.parentLocator.onChanged; + } + + constructor(private readonly parentLocator: ILocator) {} + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const didUpdate = new EventEmitter | ProgressNotificationEvent>(); + const incomingIterator = this.parentLocator.iterEnvs(query); + const iterator = iterEnvsIterator(incomingIterator, didUpdate); + iterator.onUpdated = didUpdate.event; + return iterator; + } +} + +async function* iterEnvsIterator( + iterator: IPythonEnvsIterator, + didUpdate: EventEmitter | ProgressNotificationEvent>, +): IPythonEnvsIterator { + const state = { + done: false, + pending: 0, + }; + const seen: BasicEnvInfo[] = []; + + if (iterator.onUpdated !== undefined) { + const listener = iterator.onUpdated((event) => { + if (isProgressEvent(event)) { + if (event.stage === ProgressReportStage.discoveryFinished) { + state.done = true; + listener.dispose(); + } else { + didUpdate.fire(event); + } + } else if (event.update === undefined) { + throw new Error( + 'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in reducer', + ); + } else if (event.index !== undefined && seen[event.index] !== undefined) { + const oldEnv = seen[event.index]; + seen[event.index] = event.update; + didUpdate.fire({ index: event.index, old: oldEnv, update: event.update }); + } else { + // This implies a problem in a downstream locator + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); + }); + } else { + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + } + + let result = await iterator.next(); + while (!result.done) { + const currEnv = result.value; + const oldIndex = seen.findIndex((s) => areSameEnv(s, currEnv)); + if (oldIndex !== -1) { + resolveDifferencesInBackground(oldIndex, currEnv, state, didUpdate, seen).ignoreErrors(); + } else { + // We haven't yielded a matching env so yield this one as-is. + yield currEnv; + seen.push(currEnv); + } + result = await iterator.next(); + } + if (iterator.onUpdated === undefined) { + state.done = true; + checkIfFinishedAndNotify(state, didUpdate); + } +} + +async function resolveDifferencesInBackground( + oldIndex: number, + newEnv: BasicEnvInfo, + state: { done: boolean; pending: number }, + didUpdate: EventEmitter | ProgressNotificationEvent>, + seen: BasicEnvInfo[], +) { + state.pending += 1; + // It's essential we increment the pending call count before any asynchronus calls in this method. + // We want this to be run even when `resolveInBackground` is called in background. + const oldEnv = seen[oldIndex]; + const merged = resolveEnvCollision(oldEnv, newEnv); + if (!isEqual(oldEnv, merged)) { + seen[oldIndex] = merged; + didUpdate.fire({ index: oldIndex, old: oldEnv, update: merged }); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); +} + +/** + * When all info from incoming iterator has been received and all background calls finishes, notify that we're done + * @param state Carries the current state of progress + * @param didUpdate Used to notify when finished + */ +function checkIfFinishedAndNotify( + state: { done: boolean; pending: number }, + didUpdate: EventEmitter | ProgressNotificationEvent>, +) { + if (state.done && state.pending === 0) { + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); + didUpdate.dispose(); + traceVerbose(`Finished with environment reducer`); + } +} + +function resolveEnvCollision(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): BasicEnvInfo { + const [env] = sortEnvInfoByPriority(oldEnv, newEnv); + const merged = cloneDeep(env); + merged.source = uniq((oldEnv.source ?? []).concat(newEnv.source ?? [])); + merged.searchLocation = getMergedSearchLocation(oldEnv, newEnv); + return merged; +} + +function getMergedSearchLocation(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): Uri | undefined { + if (oldEnv.searchLocation && newEnv.searchLocation) { + // Choose the deeper project path of the two, as that can be used to signify + // that the environment is related to both the projects. + if (isParentPath(oldEnv.searchLocation.fsPath, newEnv.searchLocation.fsPath)) { + return oldEnv.searchLocation; + } + if (isParentPath(newEnv.searchLocation.fsPath, oldEnv.searchLocation.fsPath)) { + return newEnv.searchLocation; + } + } + return oldEnv.searchLocation ?? newEnv.searchLocation; +} + +/** + * Selects an environment based on the environment selection priority. This should + * match the priority in the environment identifier. + */ +function sortEnvInfoByPriority(...envs: BasicEnvInfo[]): BasicEnvInfo[] { + // TODO: When we consolidate the PythonEnvKind and EnvironmentType we should have + // one location where we define priority. + const envKindByPriority: PythonEnvKind[] = getPrioritizedEnvKinds(); + return envs.sort( + (a: BasicEnvInfo, b: BasicEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind), + ); +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts new file mode 100644 index 000000000000..6bd342d14d9c --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep } from 'lodash'; +import { Event, EventEmitter } from 'vscode'; +import { isIdentifierRegistered, identifyEnvironment } from '../../../common/environmentIdentifier'; +import { IEnvironmentInfoService } from '../../info/environmentInfoService'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; +import { getEnvPath, setEnvDisplayString } from '../../info/env'; +import { InterpreterInformation } from '../../info/interpreter'; +import { + BasicEnvInfo, + ICompositeLocator, + IPythonEnvsIterator, + IResolvingLocator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../locator'; +import { PythonEnvsChangedEvent } from '../../watcher'; +import { resolveBasicEnv } from './resolverUtils'; +import { traceVerbose, traceWarn } from '../../../../logging'; +import { getEnvironmentDirFromPath, getInterpreterPathFromDir, isPythonExecutable } from '../../../common/commonUtils'; +import { getEmptyVersion } from '../../info/pythonVersion'; + +/** + * Calls environment info service which runs `interpreterInfo.py` script on environments received + * from the parent locator. Uses information received to populate environments further and pass it on. + */ +export class PythonEnvsResolver implements IResolvingLocator { + public get onChanged(): Event { + return this.parentLocator.onChanged; + } + + constructor( + private readonly parentLocator: ICompositeLocator, + private readonly environmentInfoService: IEnvironmentInfoService, + ) { + this.parentLocator.onChanged((event) => { + if (event.type && event.searchLocation !== undefined) { + // We detect an environment changed, reset any stored info for it so it can be re-run. + this.environmentInfoService.resetInfo(event.searchLocation); + } + }); + } + + public async resolveEnv(path: string): Promise { + const [executablePath, envPath] = await getExecutablePathAndEnvPath(path); + path = executablePath.length ? executablePath : envPath; + const kind = await identifyEnvironment(path); + const environment = await resolveBasicEnv({ kind, executablePath, envPath }); + const info = await this.environmentInfoService.getEnvironmentInfo(environment); + traceVerbose( + `Environment resolver resolved ${path} for ${JSON.stringify(environment)} to ${JSON.stringify(info)}`, + ); + if (!info) { + return undefined; + } + return getResolvedEnv(info, environment); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const didUpdate = new EventEmitter(); + const incomingIterator = this.parentLocator.iterEnvs(query); + const iterator = this.iterEnvsIterator(incomingIterator, didUpdate); + iterator.onUpdated = didUpdate.event; + return iterator; + } + + private async *iterEnvsIterator( + iterator: IPythonEnvsIterator, + didUpdate: EventEmitter, + ): IPythonEnvsIterator { + const environmentKinds = new Map(); + const state = { + done: false, + pending: 0, + }; + const seen: PythonEnvInfo[] = []; + + if (iterator.onUpdated !== undefined) { + const listener = iterator.onUpdated(async (event) => { + state.pending += 1; + if (isProgressEvent(event)) { + if (event.stage === ProgressReportStage.discoveryFinished) { + didUpdate.fire({ stage: ProgressReportStage.allPathsDiscovered }); + state.done = true; + listener.dispose(); + } else { + didUpdate.fire(event); + } + } else if (event.update === undefined) { + throw new Error( + 'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in resolver', + ); + } else if (event.index !== undefined && seen[event.index] !== undefined) { + const old = seen[event.index]; + await setKind(event.update, environmentKinds); + seen[event.index] = await resolveBasicEnv(event.update); + didUpdate.fire({ old, index: event.index, update: seen[event.index] }); + this.resolveInBackground(event.index, state, didUpdate, seen).ignoreErrors(); + } else { + // This implies a problem in a downstream locator + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); + }); + } else { + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + } + + let result = await iterator.next(); + while (!result.done) { + // Use cache from the current refresh where possible. + await setKind(result.value, environmentKinds); + const currEnv = await resolveBasicEnv(result.value); + seen.push(currEnv); + yield currEnv; + this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors(); + result = await iterator.next(); + } + if (iterator.onUpdated === undefined) { + state.done = true; + checkIfFinishedAndNotify(state, didUpdate); + } + } + + private async resolveInBackground( + envIndex: number, + state: { done: boolean; pending: number }, + didUpdate: EventEmitter, + seen: PythonEnvInfo[], + ) { + state.pending += 1; + // It's essential we increment the pending call count before any asynchronus calls in this method. + // We want this to be run even when `resolveInBackground` is called in background. + const info = await this.environmentInfoService.getEnvironmentInfo(seen[envIndex]); + const old = seen[envIndex]; + if (info) { + const resolvedEnv = getResolvedEnv(info, seen[envIndex], old.identifiedUsingNativeLocator); + seen[envIndex] = resolvedEnv; + didUpdate.fire({ old, index: envIndex, update: resolvedEnv }); + } else { + // Send update that the environment is not valid. + didUpdate.fire({ old, index: envIndex, update: undefined }); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); + } +} + +async function setKind(env: BasicEnvInfo, environmentKinds: Map) { + const { path } = getEnvPath(env.executablePath, env.envPath); + // For native locators, do not try to identify the environment kind. + // its already set by the native locator & thats accurate. + if (env.identifiedUsingNativeLocator) { + environmentKinds.set(path, env.kind); + return; + } + let kind = environmentKinds.get(path); + if (!kind) { + if (!isIdentifierRegistered(env.kind)) { + // If identifier is not registered, skip setting env kind. + return; + } + kind = await identifyEnvironment(path); + environmentKinds.set(path, kind); + } + env.kind = kind; +} + +/** + * When all info from incoming iterator has been received and all background calls finishes, notify that we're done + * @param state Carries the current state of progress + * @param didUpdate Used to notify when finished + */ +function checkIfFinishedAndNotify( + state: { done: boolean; pending: number }, + didUpdate: EventEmitter, +) { + if (state.done && state.pending === 0) { + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); + didUpdate.dispose(); + traceVerbose(`Finished with environment resolver`); + } +} + +function getResolvedEnv( + interpreterInfo: InterpreterInformation, + environment: PythonEnvInfo, + identifiedUsingNativeLocator = false, +) { + // Deep copy into a new object + const resolvedEnv = cloneDeep(environment); + resolvedEnv.executable.sysPrefix = interpreterInfo.executable.sysPrefix; + const isEnvLackingPython = + getEnvPath(resolvedEnv.executable.filename, resolvedEnv.location).pathType === 'envFolderPath'; + // TODO: Shouldn't this only apply to conda, how else can we have an environment and not have Python in it? + // If thats the case, then this should be gated on environment.kind === PythonEnvKind.Conda + // For non-native do not blow away the versions returned by native locator. + // Windows Store and Home brew have exe and sysprefix in different locations, + // Thus above check is not valid for these envs. + if (isEnvLackingPython && environment.kind !== PythonEnvKind.MicrosoftStore && !identifiedUsingNativeLocator) { + // Install python later into these envs might change the version, which can be confusing for users. + // So avoid displaying any version until it is installed. + resolvedEnv.version = getEmptyVersion(); + } else { + resolvedEnv.version = interpreterInfo.version; + } + resolvedEnv.arch = interpreterInfo.arch; + // Display name should be set after all the properties as we need other properties to build display name. + setEnvDisplayString(resolvedEnv); + return resolvedEnv; +} + +async function getExecutablePathAndEnvPath(path: string) { + let executablePath: string; + let envPath: string; + const isPathAnExecutable = await isPythonExecutable(path).catch((ex) => { + traceWarn('Failed to check if', path, 'is an executable', ex); + // This could happen if the path doesn't exist on a file system, but + // it still maybe the case that it's a valid file when run using a + // shell, as shells may resolve the file extensions before running it, + // so assume it to be an executable. + return true; + }); + if (isPathAnExecutable) { + executablePath = path; + envPath = getEnvironmentDirFromPath(executablePath); + } else { + envPath = path; + executablePath = (await getInterpreterPathFromDir(envPath)) ?? ''; + } + return [executablePath, envPath]; +} diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts new file mode 100644 index 000000000000..088ae9cc97c1 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { uniq } from 'lodash'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonEnvType, + UNKNOWN_PYTHON_VERSION, + virtualEnvKinds, +} from '../../info'; +import { buildEnvInfo, comparePythonVersionSpecificity, setEnvDisplayString, getEnvID } from '../../info/env'; +import { getEnvironmentDirFromPath, getPythonVersionFromPath } from '../../../common/commonUtils'; +import { arePathsSame, getFileInfo, isParentPath } from '../../../common/externalDependencies'; +import { + AnacondaCompanyName, + Conda, + getCondaInterpreterPath, + getPythonVersionFromConda, + isCondaEnvironment, +} from '../../../common/environmentManagers/conda'; +import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv'; +import { Architecture, getOSType, OSType } from '../../../../common/utils/platform'; +import { getPythonVersionFromPath as parsePythonVersionFromPath, parseVersion } from '../../info/pythonVersion'; +import { getRegistryInterpreters, getRegistryInterpretersSync } from '../../../common/windowsUtils'; +import { BasicEnvInfo } from '../../locator'; +import { parseVersionFromExecutable } from '../../info/executable'; +import { traceError, traceWarn } from '../../../../logging'; +import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; +import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; +import { ActiveState } from '../../../common/environmentManagers/activestate'; + +function getResolvers(): Map Promise> { + const resolvers = new Map Promise>(); + Object.values(PythonEnvKind).forEach((k) => { + resolvers.set(k, resolveGloballyInstalledEnv); + }); + virtualEnvKinds.forEach((k) => { + resolvers.set(k, resolveSimpleEnv); + }); + resolvers.set(PythonEnvKind.Conda, resolveCondaEnv); + resolvers.set(PythonEnvKind.MicrosoftStore, resolveMicrosoftStoreEnv); + resolvers.set(PythonEnvKind.Pyenv, resolvePyenvEnv); + resolvers.set(PythonEnvKind.ActiveState, resolveActiveStateEnv); + return resolvers; +} + +/** + * Find as much info about the given Basic Python env as possible without running the + * executable and returns it. Notice `undefined` is never returned, so environment + * returned could still be invalid. + */ +export async function resolveBasicEnv(env: BasicEnvInfo): Promise { + const { kind, source, searchLocation } = env; + const resolvers = getResolvers(); + const resolverForKind = resolvers.get(kind)!; + const resolvedEnv = await resolverForKind(env); + resolvedEnv.searchLocation = getSearchLocation(resolvedEnv, searchLocation); + resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? [])); + if ( + !env.identifiedUsingNativeLocator && + getOSType() === OSType.Windows && + resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry) + ) { + // We can update env further using information we can get from the Windows registry. + await updateEnvUsingRegistry(resolvedEnv); + } + setEnvDisplayString(resolvedEnv); + if (env.arch && !resolvedEnv.arch) { + resolvedEnv.arch = env.arch; + } + if (env.ctime && env.mtime) { + resolvedEnv.executable.ctime = env.ctime; + resolvedEnv.executable.mtime = env.mtime; + } else { + const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); + resolvedEnv.executable.ctime = ctime; + resolvedEnv.executable.mtime = mtime; + } + if (!env.identifiedUsingNativeLocator) { + const type = await getEnvType(resolvedEnv); + if (type) { + resolvedEnv.type = type; + } + } + return resolvedEnv; +} + +async function getEnvType(env: PythonEnvInfo) { + if (env.type) { + return env.type; + } + if (await isVirtualEnvironment(env.executable.filename)) { + return PythonEnvType.Virtual; + } + if (await isCondaEnvironment(env.executable.filename)) { + return PythonEnvType.Conda; + } + return undefined; +} + +function getSearchLocation(env: PythonEnvInfo, searchLocation: Uri | undefined): Uri | undefined { + if (searchLocation) { + // A search location has already been established by the downstream locators, simply use that. + return searchLocation; + } + const folders = getWorkspaceFolderPaths(); + const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f)); + if (isRootedEnv) { + // For environments inside roots, we need to set search location so they can be queried accordingly. + // In certain usecases environment directory can itself be a root, for eg. `python -m venv .`. + // So choose folder to environment path to search for this env. + // + // |__ env <--- Default search location directory + // |__ bin or Scripts + // |__ python <--- executable + return Uri.file(env.location); + } + return undefined; +} + +async function updateEnvUsingRegistry(env: PythonEnvInfo): Promise { + // Environment source has already been identified as windows registry, so we expect windows registry + // cache to already be populated. Call sync function which relies on cache. + let interpreters = getRegistryInterpretersSync(); + if (!interpreters) { + traceError('Expected registry interpreter cache to be initialized already'); + interpreters = await getRegistryInterpreters(); + } + const data = interpreters.find((i) => arePathsSame(i.interpreterPath, env.executable.filename)); + if (data) { + const versionStr = data.versionStr ?? data.sysVersionStr ?? data.interpreterPath; + let version; + try { + version = parseVersion(versionStr); + } catch (ex) { + version = UNKNOWN_PYTHON_VERSION; + } + env.kind = env.kind === PythonEnvKind.Unknown ? PythonEnvKind.OtherGlobal : env.kind; + env.version = comparePythonVersionSpecificity(version, env.version) > 0 ? version : env.version; + env.distro.defaultDisplayName = data.companyDisplayName; + env.arch = data.bitnessStr === '32bit' ? Architecture.x86 : Architecture.x64; + env.distro.org = data.distroOrgName ?? env.distro.org; + env.source = uniq(env.source.concat(PythonEnvSource.WindowsRegistry)); + } else { + traceWarn('Expected registry to find the interpreter as source was set'); + } +} + +async function resolveGloballyInstalledEnv(env: BasicEnvInfo): Promise { + const { executablePath } = env; + let version; + try { + version = env.identifiedUsingNativeLocator ? env.version : parseVersionFromExecutable(executablePath); + } catch { + version = UNKNOWN_PYTHON_VERSION; + } + const envInfo = buildEnvInfo({ + kind: env.kind, + name: env.name, + display: env.displayName, + sysPrefix: env.envPath, + location: env.envPath, + searchLocation: env.searchLocation, + version, + executable: executablePath, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + }); + return envInfo; +} + +async function resolveSimpleEnv(env: BasicEnvInfo): Promise { + const { executablePath, kind } = env; + const envInfo = buildEnvInfo({ + kind, + version: env.identifiedUsingNativeLocator ? env.version : await getPythonVersionFromPath(executablePath), + executable: executablePath, + sysPrefix: env.envPath, + location: env.envPath, + display: env.displayName, + searchLocation: env.searchLocation, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + name: env.name, + type: PythonEnvType.Virtual, + }); + const location = env.envPath ?? getEnvironmentDirFromPath(executablePath); + envInfo.location = location; + envInfo.name = path.basename(location); + return envInfo; +} + +async function resolveCondaEnv(env: BasicEnvInfo): Promise { + if (env.identifiedUsingNativeLocator) { + // New approach using native locator. + const executable = env.executablePath; + const envPath = env.envPath ?? getEnvironmentDirFromPath(executable); + // TODO: Hacky, `executable` is never undefined in the typedef, + // However, in reality with native locator this can be undefined. + const version = env.version ?? (executable ? await getPythonVersionFromPath(executable) : undefined); + const info = buildEnvInfo({ + executable, + kind: PythonEnvKind.Conda, + org: AnacondaCompanyName, + location: envPath, + sysPrefix: envPath, + display: env.displayName, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + searchLocation: env.searchLocation, + source: [], + version, + type: PythonEnvType.Conda, + name: env.name, + }); + + if (env.envPath && executable && path.basename(executable) === executable) { + // For environments without python, set ID using the predicted executable path after python is installed. + // Another alternative could've been to set ID of all conda environments to the environment path, as that + // remains constant even after python installation. + const predictedExecutable = getCondaInterpreterPath(env.envPath); + info.id = getEnvID(predictedExecutable, env.envPath); + } + return info; + } + + // Old approach (without native locator). + // In this approach we need to find conda. + const { executablePath } = env; + const conda = await Conda.getConda(); + if (conda === undefined) { + traceWarn(`${executablePath} identified as Conda environment even though Conda is not found`); + // Environment could still be valid, resolve as a simple env. + env.kind = PythonEnvKind.Unknown; + const envInfo = await resolveSimpleEnv(env); + envInfo.type = PythonEnvType.Conda; + // Assume it's a prefixed env by default because prefixed CLIs work even for named environments. + envInfo.name = ''; + return envInfo; + } + + const envPath = env.envPath ?? getEnvironmentDirFromPath(env.executablePath); + let executable: string; + if (env.executablePath.length > 0) { + executable = env.executablePath; + } else { + executable = await conda.getInterpreterPathForEnvironment({ prefix: envPath }); + } + const version = executable ? await getPythonVersionFromConda(executable) : undefined; + const info = buildEnvInfo({ + executable, + kind: PythonEnvKind.Conda, + org: AnacondaCompanyName, + location: envPath, + source: [], + version, + type: PythonEnvType.Conda, + name: env.name ?? (await conda?.getName(envPath)), + }); + + if (env.envPath && path.basename(executable) === executable) { + // For environments without python, set ID using the predicted executable path after python is installed. + // Another alternative could've been to set ID of all conda environments to the environment path, as that + // remains constant even after python installation. + const predictedExecutable = getCondaInterpreterPath(env.envPath); + info.id = getEnvID(predictedExecutable, env.envPath); + } + return info; +} + +async function resolvePyenvEnv(env: BasicEnvInfo): Promise { + const { executablePath } = env; + const location = env.envPath ?? getEnvironmentDirFromPath(executablePath); + const name = path.basename(location); + + // The sub-directory name sometimes can contain distro and python versions. + // here we attempt to extract the texts out of the name. + const versionStrings = parsePyenvVersion(name); + + const envInfo = buildEnvInfo({ + // If using native resolver, then we can get the kind from the native resolver. + // E.g. pyenv can have conda environments as well. + kind: env.identifiedUsingNativeLocator && env.kind ? env.kind : PythonEnvKind.Pyenv, + executable: executablePath, + source: [], + location, + searchLocation: env.searchLocation, + sysPrefix: env.envPath, + display: env.displayName, + name: env.name, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + // Pyenv environments can fall in to these three categories: + // 1. Global Installs : These are environments that are created when you install + // a supported python distribution using `pyenv install ` command. + // These behave similar to globally installed version of python or distribution. + // + // 2. Virtual Envs : These are environments that are created when you use + // `pyenv virtualenv `. These are similar to environments + // created using `python -m venv `. + // + // 3. Conda Envs : These are environments that are created when you use + // `pyenv virtualenv `. These are similar to + // environments created using `conda create -n . + // + // All these environments are fully handled by `pyenv` and should be activated using + // `pyenv local|global ` or `pyenv shell ` + // + // Here we look for near by files, or config files to see if we can get python version info + // without running python itself. + version: env.version ?? (await getPythonVersionFromPath(executablePath, versionStrings?.pythonVer)), + org: versionStrings && versionStrings.distro ? versionStrings.distro : '', + }); + + // Do this only for the old approach, when not using native locators. + if (!env.identifiedUsingNativeLocator) { + if (await isBaseCondaPyenvEnvironment(executablePath)) { + envInfo.name = 'base'; + } else { + envInfo.name = name; + } + } + return envInfo; +} + +async function resolveActiveStateEnv(env: BasicEnvInfo): Promise { + const info = buildEnvInfo({ + kind: env.kind, + executable: env.executablePath, + display: env.displayName, + version: env.version, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + location: env.envPath, + name: env.name, + searchLocation: env.searchLocation, + sysPrefix: env.envPath, + }); + const projects = await ActiveState.getState().then((v) => v?.getProjects()); + if (projects) { + for (const project of projects) { + for (const dir of project.executables) { + if (arePathsSame(dir, path.dirname(env.executablePath))) { + info.name = `${project.organization}/${project.name}`; + return info; + } + } + } + } + return info; +} + +async function isBaseCondaPyenvEnvironment(executablePath: string) { + if (!(await isCondaEnvironment(executablePath))) { + return false; + } + const location = getEnvironmentDirFromPath(executablePath); + const pyenvVersionDir = getPyenvVersionsDir(); + return arePathsSame(path.dirname(location), pyenvVersionDir); +} + +async function resolveMicrosoftStoreEnv(env: BasicEnvInfo): Promise { + const { executablePath } = env; + return buildEnvInfo({ + kind: PythonEnvKind.MicrosoftStore, + executable: executablePath, + version: env.version ?? parsePythonVersionFromPath(executablePath), + org: 'Microsoft', + display: env.displayName, + location: env.envPath, + sysPrefix: env.envPath, + searchLocation: env.searchLocation, + name: env.name, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + arch: Architecture.x64, + source: [PythonEnvSource.PathEnvVar], + }); +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts new file mode 100644 index 000000000000..3fbdacc639a5 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ActiveState } from '../../../common/environmentManagers/activestate'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { findInterpretersInDir } from '../../../common/commonUtils'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export class ActiveStateLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'activestate'; + + // eslint-disable-next-line class-methods-use-this + public async *doIterEnvs(): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + const state = await ActiveState.getState(); + if (state === undefined) { + traceVerbose(`Couldn't locate the state binary.`); + return; + } + traceInfo(`Searching for active state environments`); + const projects = await state.getProjects(); + if (projects === undefined) { + traceVerbose(`Couldn't fetch State Tool projects.`); + return; + } + for (const project of projects) { + if (project.executables) { + for (const dir of project.executables) { + try { + traceVerbose(`Looking for Python in: ${project.name}`); + for await (const exe of findInterpretersInDir(dir)) { + traceVerbose(`Found Python executable: ${exe.filename}`); + yield { kind: PythonEnvKind.ActiveState, executablePath: exe.filename }; + } + } catch (ex) { + traceError(`Failed to process State Tool project: ${JSON.stringify(project)}`, ex); + } + } + } + } + traceInfo(`Finished searching for active state environments: ${stopWatch.elapsedTime} milliseconds`); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts new file mode 100644 index 000000000000..bb48ba75b9dd --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import '../../../../common/extensions'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { Conda, getCondaEnvironmentsTxt } from '../../../common/environmentManagers/conda'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export class CondaEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'conda-envs'; + + public constructor() { + super( + () => getCondaEnvironmentsTxt(), + async () => PythonEnvKind.Conda, + { isFile: true }, + ); + } + + // eslint-disable-next-line class-methods-use-this + public async *doIterEnvs(_: unknown): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for conda environments'); + const conda = await Conda.getConda(); + if (conda === undefined) { + traceVerbose(`Couldn't locate the conda binary.`); + return; + } + traceVerbose(`Searching for conda environments using ${conda.command}`); + + const envs = await conda.getEnvList(); + for (const env of envs) { + try { + traceVerbose(`Looking into conda env for executable: ${JSON.stringify(env)}`); + const executablePath = await conda.getInterpreterPathForEnvironment(env); + traceVerbose(`Found conda executable: ${executablePath}`); + yield { kind: PythonEnvKind.Conda, executablePath, envPath: env.prefix }; + } catch (ex) { + traceError(`Failed to process conda env: ${JSON.stringify(env)}`, ex); + } + } + traceInfo(`Finished searching for conda environments: ${stopWatch.elapsedTime} milliseconds`); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts new file mode 100644 index 000000000000..6aa83bbc376b --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { uniq } from 'lodash'; +import * as path from 'path'; +import { chain, iterable } from '../../../../common/utils/async'; +import { getUserHomeDir } from '../../../../common/utils/platform'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; +import { getPythonSetting, onDidChangePythonSetting, pathExists } from '../../../common/externalDependencies'; +import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; +import { + isVenvEnvironment, + isVirtualenvEnvironment, + isVirtualenvwrapperEnvironment, +} from '../../../common/environmentManagers/simplevirtualenvs'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; +/** + * Default number of levels of sub-directories to recurse when looking for interpreters. + */ +const DEFAULT_SEARCH_DEPTH = 2; + +export const VENVPATH_SETTING_KEY = 'venvPath'; +export const VENVFOLDERS_SETTING_KEY = 'venvFolders'; + +/** + * Gets all custom virtual environment locations to look for environments. + */ +async function getCustomVirtualEnvDirs(): Promise { + const venvDirs: string[] = []; + const venvPath = getPythonSetting(VENVPATH_SETTING_KEY); + if (venvPath) { + venvDirs.push(untildify(venvPath)); + } + const venvFolders = getPythonSetting(VENVFOLDERS_SETTING_KEY) ?? []; + const homeDir = getUserHomeDir(); + if (homeDir && (await pathExists(homeDir))) { + venvFolders + .map((item) => (item.startsWith(homeDir) ? item : path.join(homeDir, item))) + .forEach((d) => venvDirs.push(d)); + venvFolders.forEach((item) => venvDirs.push(untildify(item))); + } + return asyncFilter(uniq(venvDirs), pathExists); +} + +/** + * Gets the virtual environment kind for a given interpreter path. + * This only checks for environments created using venv, virtualenv, + * and virtualenvwrapper based environments. + * @param interpreterPath: Absolute path to the interpreter paths. + */ +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPipenvEnvironment(interpreterPath)) { + return PythonEnvKind.Pipenv; + } + + if (await isVirtualenvwrapperEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnvWrapper; + } + + if (await isVenvEnvironment(interpreterPath)) { + return PythonEnvKind.Venv; + } + + if (await isVirtualenvEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnv; + } + + return PythonEnvKind.Unknown; +} + +/** + * Finds and resolves custom virtual environments that users have provided. + */ +export class CustomVirtualEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'custom-virtual-envs'; + + constructor() { + super(getCustomVirtualEnvDirs, getVirtualEnvKind, { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. However even + // if the type detected is incorrect, it doesn't do any practical harm as kinds + // in this locator are used in the same way (same activation commands etc.) + delayOnCreated: 1000, + }); + } + + protected async initResources(): Promise { + this.disposables.push(onDidChangePythonSetting(VENVPATH_SETTING_KEY, () => this.fire())); + this.disposables.push(onDidChangePythonSetting(VENVFOLDERS_SETTING_KEY, () => this.fire())); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator() { + const stopWatch = new StopWatch(); + traceInfo('Searching for custom virtual environments'); + const envRootDirs = await getCustomVirtualEnvDirs(); + const envGenerators = envRootDirs.map((envRootDir) => { + async function* generator() { + traceVerbose(`Searching for custom virtual envs in: ${envRootDir}`); + + const executables = findInterpretersInDir(envRootDir, DEFAULT_SEARCH_DEPTH); + + for await (const entry of executables) { + const { filename } = entry; + // We only care about python.exe (on windows) and python (on linux/mac) + // Other version like python3.exe or python3.8 are often symlinks to + // python.exe or python in the same directory in the case of virtual + // environments. + if (await looksLikeBasicVirtualPython(entry)) { + try { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(filename); + yield { kind, executablePath: filename }; + traceVerbose(`Custom Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } else { + traceVerbose(`Custom Virtual Environment: [skipped] ${filename}`); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceInfo(`Finished searching for custom virtual envs: ${stopWatch.elapsedTime} milliseconds`); + } + + return iterator(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts new file mode 100644 index 000000000000..8a2b857d496a --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { getPythonSetting, onDidChangePythonSetting } from '../../../common/externalDependencies'; +import '../../../../common/extensions'; +import { traceVerbose } from '../../../../logging'; +import { DEFAULT_INTERPRETER_SETTING } from '../../../../common/constants'; + +export const DEFAULT_INTERPRETER_PATH_SETTING_KEY = 'defaultInterpreterPath'; + +/** + * Finds and resolves custom virtual environments that users have provided. + */ +export class CustomWorkspaceLocator extends FSWatchingLocator { + public readonly providerId: string = 'custom-workspace-locator'; + + constructor(private readonly root: string) { + super( + () => [], + async () => PythonEnvKind.Unknown, + ); + } + + protected async initResources(): Promise { + this.disposables.push( + onDidChangePythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, () => this.fire(), this.root), + ); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator { + const iterator = async function* (root: string) { + traceVerbose('Searching for custom workspace envs'); + const filename = getPythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, root); + if (!filename || filename === DEFAULT_INTERPRETER_SETTING) { + // If the user has not set a custom interpreter, our job is done. + return; + } + yield { kind: PythonEnvKind.Unknown, executablePath: filename }; + traceVerbose(`Finished searching for custom workspace envs`); + }; + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/filesLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/filesLocator.ts new file mode 100644 index 000000000000..e5ed206650ca --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/filesLocator.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable max-classes-per-file */ + +import { Event } from 'vscode'; +import { iterPythonExecutablesInDir } from '../../../common/commonUtils'; +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../../locator'; +import { PythonEnvsChangedEvent, PythonEnvsWatcher } from '../../watcher'; + +type GetExecutablesFunc = () => AsyncIterableIterator; + +/** + * A naive locator the wraps a function that finds Python executables. + */ +abstract class FoundFilesLocator implements ILocator { + public abstract readonly providerId: string; + + public readonly onChanged: Event; + + protected readonly watcher = new PythonEnvsWatcher(); + + constructor( + private readonly kind: PythonEnvKind, + private readonly getExecutables: GetExecutablesFunc, + private readonly source?: PythonEnvSource[], + ) { + this.onChanged = this.watcher.onChanged; + } + + public iterEnvs(_query?: PythonLocatorQuery): IPythonEnvsIterator { + const executables = this.getExecutables(); + async function* generator(kind: PythonEnvKind, source?: PythonEnvSource[]): IPythonEnvsIterator { + for await (const executablePath of executables) { + yield { executablePath, kind, source }; + } + } + const iterator = generator(this.kind, this.source); + return iterator; + } +} + +type GetDirExecutablesFunc = (dir: string) => AsyncIterableIterator; + +/** + * A locator for executables in a single directory. + */ +export class DirFilesLocator extends FoundFilesLocator { + public readonly providerId: string; + + constructor( + dirname: string, + defaultKind: PythonEnvKind, + // This is put in a closure and otherwise passed through as-is. + getExecutables: GetDirExecutablesFunc = getExecutablesDefault, + source?: PythonEnvSource[], + ) { + super(defaultKind, () => getExecutables(dirname), source); + this.providerId = `dir-files-${dirname}`; + } +} + +// For now we do not have a DirFilesWatchingLocator. It would be +// a subclass of FSWatchingLocator that wraps a DirFilesLocator +// instance. + +async function* getExecutablesDefault(dirname: string): AsyncIterableIterator { + for await (const entry of iterPythonExecutablesInDir(dirname)) { + yield entry.filename; + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts new file mode 100644 index 000000000000..dd7db5538565 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { FileChangeType, watchLocationForPattern } from '../../../../common/platform/fileSystemWatcher'; +import { sleep } from '../../../../common/utils/async'; +import { traceVerbose, traceWarn } from '../../../../logging'; +import { getEnvironmentDirFromPath } from '../../../common/commonUtils'; +import { + PythonEnvStructure, + resolvePythonExeGlobs, + watchLocationForPythonBinaries, +} from '../../../common/pythonBinariesWatcher'; +import { PythonEnvKind } from '../../info'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; + +export enum FSWatcherKind { + Global, // Watcher observes a global location such as ~/.envs, %LOCALAPPDATA%/Microsoft/WindowsApps. + Workspace, // Watchers observes directory in the user's currently open workspace. +} + +type DirUnwatchableReason = 'directory does not exist' | 'too many files' | undefined; + +/** + * Determine if the directory is watchable. + */ +function checkDirWatchable(dirname: string): DirUnwatchableReason { + let names: string[]; + try { + names = fs.readdirSync(dirname); + } catch (err) { + const exception = err as NodeJS.ErrnoException; + traceVerbose('Reading directory failed', exception); + if (exception.code === 'ENOENT') { + // Treat a missing directory as unwatchable since it can lead to CPU load issues: + // https://github.com/microsoft/vscode-python/issues/18459 + return 'directory does not exist'; + } + return undefined; + } + // The limit here is an educated guess. + if (names.length > 200) { + return 'too many files'; + } + return undefined; +} + +type LocationWatchOptions = { + /** + * Glob which represents basename of the executable or directory to watch. + */ + baseGlob?: string; + /** + * Time to wait before handling an environment-created event. + */ + delayOnCreated?: number; // milliseconds + /** + * Location affected by the event. If not provided, a default search location is used. + */ + searchLocation?: string; + /** + * The Python env structure to watch. + */ + envStructure?: PythonEnvStructure; +}; + +type FileWatchOptions = { + /** + * If the provided root is a file instead. In this case the file is directly watched instead for + * looking for python binaries inside a root. + */ + isFile: boolean; +}; + +/** + * The base for Python envs locators who watch the file system. + * Most low-level locators should be using this. + * + * Subclasses can call `this.emitter.fire()` * to emit events. + */ +export abstract class FSWatchingLocator extends LazyResourceBasedLocator { + constructor( + /** + * Location(s) to watch for python binaries. + */ + private readonly getRoots: () => Promise | string | string[], + /** + * Returns the kind of environment specific to locator given the path to executable. + */ + private readonly getKind: (executable: string) => Promise, + private readonly creationOptions: LocationWatchOptions | FileWatchOptions = {}, + private readonly watcherKind: FSWatcherKind = FSWatcherKind.Global, + ) { + super(); + this.activate().ignoreErrors(); + } + + protected async initWatchers(): Promise { + // Enable all workspace watchers. + if (this.watcherKind === FSWatcherKind.Global && !isWatchingAFile(this.creationOptions)) { + // Do not allow global location watchers for now. + return; + } + + // Start the FS watchers. + let roots = await this.getRoots(); + if (typeof roots === 'string') { + roots = [roots]; + } + const promises = roots.map(async (root) => { + if (isWatchingAFile(this.creationOptions)) { + return root; + } + // Note that we only check the root dir. Any directories + // that might be watched due to a glob are not checked. + const unwatchable = await checkDirWatchable(root); + if (unwatchable) { + traceWarn(`Dir "${root}" is not watchable (${unwatchable})`); + return undefined; + } + return root; + }); + const watchableRoots = (await Promise.all(promises)).filter((root) => !!root) as string[]; + watchableRoots.forEach((root) => this.startWatchers(root)); + } + + protected fire(args = {}): void { + this.emitter.fire({ ...args, providerId: this.providerId }); + } + + private startWatchers(root: string): void { + const opts = this.creationOptions; + if (isWatchingAFile(opts)) { + traceVerbose('Start watching file for changes', root); + this.disposables.push( + watchLocationForPattern(path.dirname(root), path.basename(root), () => { + traceVerbose('Detected change in file: ', root, 'initiating a refresh'); + this.emitter.fire({ providerId: this.providerId }); + }), + ); + return; + } + const callback = async (type: FileChangeType, executable: string) => { + if (type === FileChangeType.Created) { + if (opts.delayOnCreated !== undefined) { + // Note detecting kind of env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. + await sleep(opts.delayOnCreated); + } + } + // Fetching kind after deletion normally fails because the file structure around the + // executable is no longer available, so ignore the errors. + const kind = await this.getKind(executable).catch(() => undefined); + // By default, search location particularly for virtual environments is intended as the + // directory in which the environment was found in. For eg. the default search location + // for an env containing 'bin' or 'Scripts' directory is: + // + // searchLocation <--- Default search location directory + // |__ env + // |__ bin or Scripts + // |__ python <--- executable + const searchLocation = Uri.file(opts.searchLocation ?? path.dirname(getEnvironmentDirFromPath(executable))); + traceVerbose('Fired event ', JSON.stringify({ type, kind, searchLocation }), 'from locator'); + this.emitter.fire({ type, kind, searchLocation, providerId: this.providerId, envPath: executable }); + }; + + const globs = resolvePythonExeGlobs( + opts.baseGlob, + // The structure determines which globs are returned. + opts.envStructure, + ); + traceVerbose('Start watching root', root, 'for globs', JSON.stringify(globs)); + const watchers = globs.map((g) => watchLocationForPythonBinaries(root, callback, g)); + this.disposables.push(...watchers); + } +} + +function isWatchingAFile(options: LocationWatchOptions | FileWatchOptions): options is FileWatchOptions { + return 'isFile' in options && options.isFile; +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts new file mode 100644 index 000000000000..86fbbed55043 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { toLower, uniq, uniqBy } from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { chain, iterable } from '../../../../common/utils/async'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { getProjectDir, isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; +import { + isVenvEnvironment, + isVirtualenvEnvironment, + isVirtualenvwrapperEnvironment, +} from '../../../common/environmentManagers/simplevirtualenvs'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; + +const DEFAULT_SEARCH_DEPTH = 2; +/** + * Gets all default virtual environment locations. This uses WORKON_HOME, + * and user home directory to find some known locations where global virtual + * environments are often created. + */ +async function getGlobalVirtualEnvDirs(): Promise { + const venvDirs: string[] = []; + + let workOnHome = getEnvironmentVariable('WORKON_HOME'); + if (workOnHome) { + workOnHome = untildify(workOnHome); + if (await pathExists(workOnHome)) { + venvDirs.push(workOnHome); + } + } + + const homeDir = getUserHomeDir(); + if (homeDir && (await pathExists(homeDir))) { + const subDirs = [ + 'envs', + 'Envs', + '.direnv', + '.venvs', + '.virtualenvs', + path.join('.local', 'share', 'virtualenvs'), + ]; + const filtered = await asyncFilter( + subDirs.map((d) => path.join(homeDir, d)), + pathExists, + ); + filtered.forEach((d) => venvDirs.push(d)); + } + + return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs); +} + +async function getSearchLocation(env: BasicEnvInfo): Promise { + if (env.kind === PythonEnvKind.Pipenv) { + // Pipenv environments are created only for a specific project, so they must only + // appear if that particular project is being queried. + const project = await getProjectDir(path.dirname(path.dirname(env.executablePath))); + if (project) { + return Uri.file(project); + } + } + return undefined; +} + +/** + * Gets the virtual environment kind for a given interpreter path. + * This only checks for environments created using venv, virtualenv, + * and virtualenvwrapper based environments. + * @param interpreterPath: Absolute path to the interpreter paths. + */ +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPipenvEnvironment(interpreterPath)) { + return PythonEnvKind.Pipenv; + } + + if (await isVirtualenvwrapperEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnvWrapper; + } + + if (await isVenvEnvironment(interpreterPath)) { + return PythonEnvKind.Venv; + } + + if (await isVirtualenvEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnv; + } + + return PythonEnvKind.Unknown; +} + +/** + * Finds and resolves virtual environments created in known global locations. + */ +export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'global-virtual-env'; + + constructor(private readonly searchDepth?: number) { + super(getGlobalVirtualEnvDirs, getVirtualEnvKind, { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. However even + // if the type detected is incorrect, it doesn't do any practical harm as kinds + // in this locator are used in the same way (same activation commands etc.) + delayOnCreated: 1000, + }); + } + + protected doIterEnvs(): IPythonEnvsIterator { + // Number of levels of sub-directories to recurse when looking for + // interpreters + const searchDepth = this.searchDepth ?? DEFAULT_SEARCH_DEPTH; + + async function* iterator() { + const stopWatch = new StopWatch(); + traceInfo('Searching for global virtual environments'); + const envRootDirs = await getGlobalVirtualEnvDirs(); + const envGenerators = envRootDirs.map((envRootDir) => { + async function* generator() { + traceVerbose(`Searching for global virtual envs in: ${envRootDir}`); + + const executables = findInterpretersInDir(envRootDir, searchDepth); + + for await (const entry of executables) { + const { filename } = entry; + // We only care about python.exe (on windows) and python (on linux/mac) + // Other version like python3.exe or python3.8 are often symlinks to + // python.exe or python in the same directory in the case of virtual + // environments. + if (await looksLikeBasicVirtualPython(entry)) { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(filename); + const searchLocation = await getSearchLocation({ kind, executablePath: filename }); + try { + yield { kind, executablePath: filename, searchLocation }; + traceVerbose(`Global Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } else { + traceVerbose(`Global Virtual Environment: [skipped] ${filename}`); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceInfo(`Finished searching for global virtual envs: ${stopWatch.elapsedTime} milliseconds`); + } + + return iterator(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts new file mode 100644 index 000000000000..f7746a8c5a2e --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts @@ -0,0 +1,57 @@ +'use strict'; + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { Hatch } from '../../../common/environmentManagers/hatch'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { traceError, traceVerbose } from '../../../../logging'; +import { chain, iterable } from '../../../../common/utils/async'; +import { getInterpreterPathFromDir } from '../../../common/commonUtils'; + +/** + * Gets all default virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const hatch = await Hatch.getHatch(root); + const envDirs = (await hatch?.getEnvList()) ?? []; + return asyncFilter(envDirs, pathExists); +} + +/** + * Finds and resolves virtual environments created using Hatch. + */ +export class HatchLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'hatch'; + + public constructor(private readonly root: string) { + super(); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for Hatch virtual envs in: ${envDir}`); + const filename = await getInterpreterPathFromDir(envDir); + if (filename !== undefined) { + try { + yield { executablePath: filename, kind: PythonEnvKind.Hatch }; + traceVerbose(`Hatch Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for Hatch envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts new file mode 100644 index 000000000000..2068a05f3a69 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as minimatch from 'minimatch'; +import * as path from 'path'; +import * as fsapi from '../../../../common/platform/fs-paths'; +import { PythonEnvKind } from '../../info'; +import { IPythonEnvsIterator, BasicEnvInfo } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { PythonEnvStructure } from '../../../common/pythonBinariesWatcher'; +import { + isStorePythonInstalled, + getMicrosoftStoreAppsRoot, +} from '../../../common/environmentManagers/microsoftStoreEnv'; +import { traceInfo } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +/** + * This is a glob pattern which matches following file names: + * python3.8.exe + * python3.9.exe + * python3.10.exe + * This pattern does not match: + * python.exe + * python2.7.exe + * python3.exe + * python38.exe + */ +const pythonExeGlob = 'python3.{[0-9],[0-9][0-9]}.exe'; + +/** + * Checks if a given path ends with python3.*.exe. Not all python executables are matched as + * we do not want to return duplicate executables. + * @param {string} interpreterPath : Path to python interpreter. + * @returns {boolean} : Returns true if the path matches pattern for windows python executable. + */ +function isMicrosoftStorePythonExePattern(interpreterPath: string): boolean { + return minimatch.default(path.basename(interpreterPath), pythonExeGlob, { nocase: true }); +} + +/** + * Gets paths to the Python executable under Microsoft Store apps. + * @returns: Returns python*.exe for the microsoft 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 getMicrosoftStorePythonExes(): Promise { + if (await isStorePythonInstalled()) { + const windowsAppsRoot = getMicrosoftStoreAppsRoot(); + + // Collect python*.exe directly under %LOCALAPPDATA%/Microsoft/WindowsApps + const files = await fsapi.readdir(windowsAppsRoot); + return files + .map((filename: string) => path.join(windowsAppsRoot, filename)) + .filter(isMicrosoftStorePythonExePattern); + } + return []; +} + +export class MicrosoftStoreLocator extends FSWatchingLocator { + public readonly providerId: string = 'microsoft-store'; + + private readonly kind: PythonEnvKind = PythonEnvKind.MicrosoftStore; + + constructor() { + // We have to watch the directory instead of the executable here because + // FS events are not triggered for `*.exe` in the WindowsApps folder. The + // .exe files here are reparse points and not real files. Watching the + // PythonSoftwareFoundation directory will trigger both for new install + // and update case. Update is handled by deleting and recreating the + // PythonSoftwareFoundation directory. + super(getMicrosoftStoreAppsRoot, async () => this.kind, { + baseGlob: pythonExeGlob, + searchLocation: getMicrosoftStoreAppsRoot(), + envStructure: PythonEnvStructure.Flat, + }); + } + + protected doIterEnvs(): IPythonEnvsIterator { + const iterator = async function* (kind: PythonEnvKind) { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows store envs'); + const exes = await getMicrosoftStorePythonExes(); + yield* exes.map(async (executablePath: string) => ({ + kind, + executablePath, + })); + traceInfo(`Finished searching for windows store envs: ${stopWatch.elapsedTime} milliseconds`); + }; + return iterator(this.kind); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts new file mode 100644 index 000000000000..f4a3886a2120 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { chain, iterable } from '../../../../common/utils/async'; +import { traceError, traceVerbose } from '../../../../logging'; +import { getCondaInterpreterPath } from '../../../common/environmentManagers/conda'; +import { pathExists } from '../../../common/externalDependencies'; +import { PythonEnvKind } from '../../info'; +import { IPythonEnvsIterator, BasicEnvInfo } from '../../locator'; +import { FSWatcherKind, FSWatchingLocator } from './fsWatchingLocator'; +import { getPixi } from '../../../common/environmentManagers/pixi'; + +/** + * Returns all virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const pixi = await getPixi(); + const envDirs = (await pixi?.getEnvList(root)) ?? []; + return asyncFilter(envDirs, pathExists); +} + +/** + * Returns all virtual environment locations to look for in a workspace. + */ +function getVirtualEnvRootDirs(root: string): string[] { + return [path.join(path.join(root, '.pixi'), 'envs')]; +} + +export class PixiLocator extends FSWatchingLocator { + public readonly providerId: string = 'pixi'; + + public constructor(private readonly root: string) { + super( + async () => getVirtualEnvRootDirs(this.root), + async () => PythonEnvKind.Pixi, + { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. + delayOnCreated: 1000, + }, + FSWatcherKind.Workspace, + ); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for Pixi virtual envs in: ${envDir}`); + const filename = await getCondaInterpreterPath(envDir); + if (filename !== undefined) { + try { + yield { + executablePath: filename, + kind: PythonEnvKind.Pixi, + envPath: envDir, + }; + + traceVerbose(`Pixi Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for Pixi envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts new file mode 100644 index 000000000000..ab1a8cf77444 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { chain, iterable } from '../../../../common/utils/async'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { getInterpreterPathFromDir } from '../../../common/commonUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { isPoetryEnvironment, localPoetryEnvDirName, Poetry } from '../../../common/environmentManagers/poetry'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceError, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; + +/** + * Gets all default virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const envDirs = [path.join(root, localPoetryEnvDirName)]; + const poetry = await Poetry.getPoetry(root); + const virtualenvs = await poetry?.getEnvList(); + if (virtualenvs) { + envDirs.push(...virtualenvs); + } + return asyncFilter(envDirs, pathExists); +} + +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPoetryEnvironment(interpreterPath)) { + return PythonEnvKind.Poetry; + } + + return PythonEnvKind.Unknown; +} + +/** + * Finds and resolves virtual environments created using poetry. + */ +export class PoetryLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'poetry'; + + public constructor(private readonly root: string) { + super(); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for poetry virtual envs in: ${envDir}`); + const filename = await getInterpreterPathFromDir(envDir); + if (filename !== undefined) { + const kind = await getVirtualEnvKind(filename); + try { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + yield { executablePath: filename, kind, searchLocation: Uri.file(root) }; + traceVerbose(`Poetry Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for poetry envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts new file mode 100644 index 000000000000..daca4b860907 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as os from 'os'; +import { gte } from 'semver'; +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator, Locator } from '../../locator'; +import { commonPosixBinPaths, getPythonBinFromPosixPaths } from '../../../common/posixUtils'; +import { isPyenvShimDir } from '../../../common/environmentManagers/pyenv'; +import { getOSType, OSType } from '../../../../common/utils/platform'; +import { isMacDefaultPythonPath } from '../../../common/environmentManagers/macDefault'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export class PosixKnownPathsLocator extends Locator { + public readonly providerId = 'posixKnownPaths'; + + private kind: PythonEnvKind = PythonEnvKind.OtherGlobal; + + public iterEnvs(): IPythonEnvsIterator { + // Flag to remove system installs of Python 2 from the list of discovered interpreters + // If on macOS Monterey or later. + // See https://github.com/microsoft/vscode-python/issues/17870. + let isMacPython2Deprecated = false; + if (getOSType() === OSType.OSX && gte(os.release(), '21.0.0')) { + isMacPython2Deprecated = true; + } + + const iterator = async function* (kind: PythonEnvKind) { + const stopWatch = new StopWatch(); + traceInfo('Searching for interpreters in posix paths locator'); + try { + // Filter out pyenv shims. They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); + let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + traceVerbose(`Found ${pythonBinaries.length} python binaries in posix paths`); + + // Filter out MacOS system installs of Python 2 if necessary. + if (isMacPython2Deprecated) { + pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); + } + + for (const bin of pythonBinaries) { + try { + yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; + } catch (ex) { + traceError(`Failed to process environment: ${bin}`, ex); + } + } + } catch (ex) { + traceError('Failed to process posix paths', ex); + } + traceInfo( + `Finished searching for interpreters in posix paths locator: ${stopWatch.elapsedTime} milliseconds`, + ); + }; + return iterator(this.kind); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts new file mode 100644 index 000000000000..e97b69c6b882 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { getInterpreterPathFromDir } from '../../../common/commonUtils'; +import { getSubDirs } from '../../../common/externalDependencies'; +import { getPyenvVersionsDir } from '../../../common/environmentManagers/pyenv'; +import { traceError, traceInfo } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +/** + * Gets all the pyenv environments. + * + * Remarks: This function looks at the /versions directory and gets + * all the environments (global or virtual) in that directory. + */ +async function* getPyenvEnvironments(): AsyncIterableIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for pyenv environments'); + try { + const pyenvVersionDir = getPyenvVersionsDir(); + + const subDirs = getSubDirs(pyenvVersionDir, { resolveSymlinks: true }); + for await (const subDirPath of subDirs) { + const interpreterPath = await getInterpreterPathFromDir(subDirPath); + + if (interpreterPath) { + try { + yield { + kind: PythonEnvKind.Pyenv, + executablePath: interpreterPath, + }; + } catch (ex) { + traceError(`Failed to process environment: ${interpreterPath}`, ex); + } + } + } + } catch (ex) { + // This is expected when pyenv is not installed + traceInfo(`pyenv is not installed`); + } + traceInfo(`Finished searching for pyenv environments: ${stopWatch.elapsedTime} milliseconds`); +} + +export class PyenvLocator extends FSWatchingLocator { + public readonly providerId: string = 'pyenv'; + + constructor() { + super(getPyenvVersionsDir, async () => PythonEnvKind.Pyenv); + } + + // eslint-disable-next-line class-methods-use-this + public doIterEnvs(): IPythonEnvsIterator { + return getPyenvEnvironments(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts new file mode 100644 index 000000000000..440d075b4071 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable max-classes-per-file */ + +import { Event } from 'vscode'; +import * as path from 'path'; +import { IDisposable } from '../../../../common/types'; +import { getSearchPathEntries } from '../../../../common/utils/exec'; +import { Disposables } from '../../../../common/utils/resourceLifecycle'; +import { isPyenvShimDir } from '../../../common/environmentManagers/pyenv'; +import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../../locator'; +import { Locators } from '../../locators'; +import { getEnvs } from '../../locatorUtils'; +import { PythonEnvsChangedEvent } from '../../watcher'; +import { DirFilesLocator } from './filesLocator'; +import { traceInfo } from '../../../../logging'; +import { inExperiment, pathExists } from '../../../common/externalDependencies'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; +import { iterPythonExecutablesInDir, looksLikeBasicGlobalPython } from '../../../common/commonUtils'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +/** + * A locator for Windows locators found under the $PATH env var. + * + * Note that we assume $PATH won't change, so we don't need to watch + * it for changes. + */ +export class WindowsPathEnvVarLocator implements ILocator, IDisposable { + public readonly providerId: string = 'windows-path-env-var-locator'; + + public readonly onChanged: Event; + + private readonly locators: Locators; + + private readonly disposables = new Disposables(); + + constructor() { + const inExp = inExperiment(DiscoveryUsingWorkers.experiment); + const dirLocators: (ILocator & IDisposable)[] = getSearchPathEntries() + .filter( + (dirname) => + // Filter out following directories: + // 1. Microsoft Store app directories: We have a store app locator that handles this. The + // python.exe available in these directories might not be python. It can be a store + // install shortcut that takes you to microsoft store. + // + // 2. Filter out pyenv shims: They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + !isMicrosoftStoreDir(dirname) && !isPyenvShimDir(dirname), + ) + // Build a locator for each directory. + .map((dirname) => getDirFilesLocator(dirname, PythonEnvKind.System, [PythonEnvSource.PathEnvVar], inExp)); + this.disposables.push(...dirLocators); + this.locators = new Locators(dirLocators); + this.onChanged = this.locators.onChanged; + } + + public async dispose(): Promise { + this.locators.dispose(); + await this.disposables.dispose(); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + // Note that we do no filtering here, including to check if files + // are valid executables. That is left to callers (e.g. composite + // locators). + async function* iterator(it: IPythonEnvsIterator) { + const stopWatch = new StopWatch(); + traceInfo(`Searching windows known paths locator`); + for await (const env of it) { + yield env; + } + traceInfo(`Finished searching windows known paths locator: ${stopWatch.elapsedTime} milliseconds`); + } + return iterator(this.locators.iterEnvs(query)); + } +} + +async function* oldGetExecutables(dirname: string): AsyncIterableIterator { + for await (const entry of iterPythonExecutablesInDir(dirname)) { + if (await looksLikeBasicGlobalPython(entry)) { + yield entry.filename; + } + } +} + +async function* getExecutables(dirname: string): AsyncIterableIterator { + const executable = path.join(dirname, 'python.exe'); + if (await pathExists(executable)) { + yield executable; + } +} + +function getDirFilesLocator( + // These are passed through to DirFilesLocator. + dirname: string, + kind: PythonEnvKind, + source?: PythonEnvSource[], + inExp?: boolean, +): ILocator & IDisposable { + // For now we do not bother using a locator that watches for changes + // in the directory. If we did then we would use + // `DirFilesWatchingLocator`, but only if not \\windows\system32 and + // the `isDirWatchable()` (from fsWatchingLocator.ts) returns true. + const executableFunc = inExp ? getExecutables : oldGetExecutables; + const locator = new DirFilesLocator(dirname, kind, executableFunc, source); + const dispose = async () => undefined; + + // Really we should be checking for symlinks or something more + // sophisticated. Also, this should be done in ReducingLocator + // rather than in each low-level locator. In the meantime we + // take a naive approach. + async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { + yield* await getEnvs(locator.iterEnvs(query)).then((res) => res); + } + return { + providerId: locator.providerId, + iterEnvs, + dispose, + onChanged: locator.onChanged, + }; +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts new file mode 100644 index 000000000000..1447c2a90767 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts @@ -0,0 +1,75 @@ +/* eslint-disable require-yield */ +/* eslint-disable no-continue */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator, Locator, PythonLocatorQuery, IEmitter } from '../../locator'; +import { getRegistryInterpreters } from '../../../common/windowsUtils'; +import { traceError, traceInfo } from '../../../../logging'; +import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; +import { PythonEnvsChangedEvent } from '../../watcher'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; +import { inExperiment } from '../../../common/externalDependencies'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export const WINDOWS_REG_PROVIDER_ID = 'windows-registry'; + +export class WindowsRegistryLocator extends Locator { + public readonly providerId: string = WINDOWS_REG_PROVIDER_ID; + + // eslint-disable-next-line class-methods-use-this + public iterEnvs( + query?: PythonLocatorQuery, + useWorkerThreads = inExperiment(DiscoveryUsingWorkers.experiment), + ): IPythonEnvsIterator { + if (useWorkerThreads) { + /** + * Windows registry is slow and often not necessary, so notify completion immediately, but use watcher + * change events to signal for any new envs which are found. + */ + if (query?.providerId === this.providerId) { + // Query via change event, so iterate all envs. + return iterateEnvs(); + } + return iterateEnvsLazily(this.emitter); + } + return iterateEnvs(); + } +} + +async function* iterateEnvsLazily(changed: IEmitter): IPythonEnvsIterator { + loadAllEnvs(changed).ignoreErrors(); +} + +async function loadAllEnvs(changed: IEmitter) { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows registry interpreters'); + changed.fire({ providerId: WINDOWS_REG_PROVIDER_ID }); + traceInfo(`Finished searching for windows registry interpreters: ${stopWatch.elapsedTime} milliseconds`); +} + +async function* iterateEnvs(): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows registry interpreters'); + const interpreters = await getRegistryInterpreters(); // Value should already be loaded at this point, so this returns immediately. + for (const interpreter of interpreters) { + try { + // Filter out Microsoft Store app directories. We have a store app locator that handles this. + // The python.exe available in these directories might not be python. It can be a store install + // shortcut that takes you to microsoft store. + if (isMicrosoftStoreDir(interpreter.interpreterPath)) { + continue; + } + const env: BasicEnvInfo = { + kind: PythonEnvKind.OtherGlobal, + executablePath: interpreter.interpreterPath, + source: [PythonEnvSource.WindowsRegistry], + }; + yield env; + } catch (ex) { + traceError(`Failed to process environment: ${interpreter}`, ex); + } + } + traceInfo(`Finished searching for windows registry interpreters: ${stopWatch.elapsedTime} milliseconds`); +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.ts new file mode 100644 index 000000000000..b815e1d30a89 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { chain, iterable } from '../../../../common/utils/async'; +import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; +import { isVenvEnvironment, isVirtualenvEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatcherKind, FSWatchingLocator } from './fsWatchingLocator'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceVerbose } from '../../../../logging'; + +/** + * Default number of levels of sub-directories to recurse when looking for interpreters. + */ +const DEFAULT_SEARCH_DEPTH = 2; + +/** + * Gets all default virtual environment locations to look for in a workspace. + */ +function getWorkspaceVirtualEnvDirs(root: string): Promise { + return asyncFilter([root, path.join(root, '.direnv')], pathExists); +} + +/** + * Gets the virtual environment kind for a given interpreter path. + * This only checks for environments created using venv, virtualenv, + * and virtualenvwrapper based environments. + * @param interpreterPath: Absolute path to the interpreter paths. + */ +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPipenvEnvironment(interpreterPath)) { + return PythonEnvKind.Pipenv; + } + + if (await isVenvEnvironment(interpreterPath)) { + return PythonEnvKind.Venv; + } + + if (await isVirtualenvEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnv; + } + + return PythonEnvKind.Unknown; +} +/** + * Finds and resolves virtual environments created in workspace roots. + */ +export class WorkspaceVirtualEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'workspaceVirtualEnvLocator'; + + public constructor(private readonly root: string) { + super( + () => getWorkspaceVirtualEnvDirs(this.root), + getVirtualEnvKind, + { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. + delayOnCreated: 1000, + }, + FSWatcherKind.Workspace, + ); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envRootDirs = await getWorkspaceVirtualEnvDirs(root); + const envGenerators = envRootDirs.map((envRootDir) => { + async function* generator() { + traceVerbose(`Searching for workspace virtual envs in: ${envRootDir}`); + + const executables = findInterpretersInDir(envRootDir, DEFAULT_SEARCH_DEPTH); + + for await (const entry of executables) { + const { filename } = entry; + // We only care about python.exe (on windows) and python (on linux/mac) + // Other version like python3.exe or python3.8 are often symlinks to + // python.exe or python in the same directory in the case of virtual + // environments. + if (await looksLikeBasicVirtualPython(entry)) { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(filename); + yield { kind, executablePath: filename }; + traceVerbose(`Workspace Virtual Environment: [added] ${filename}`); + } else { + traceVerbose(`Workspace Virtual Environment: [skipped] ${filename}`); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for workspace virtual envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/wrappers.ts b/src/client/pythonEnvironments/base/locators/wrappers.ts new file mode 100644 index 000000000000..bfaede584f6f --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/wrappers.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { Uri } from 'vscode'; +import { IDisposable } from '../../../common/types'; +import { iterEmpty } from '../../../common/utils/async'; +import { getURIFilter } from '../../../common/utils/misc'; +import { Disposables } from '../../../common/utils/resourceLifecycle'; +import { PythonEnvInfo } from '../info'; +import { BasicEnvInfo, ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../locator'; +import { combineIterators, Locators } from '../locators'; +import { LazyResourceBasedLocator } from './common/resourceBasedLocator'; + +/** + * A wrapper around all locators used by the extension. + */ + +export class ExtensionLocators extends Locators { + constructor( + // These are expected to be low-level locators (e.g. system). + private readonly nonWorkspace: ILocator[], + // This is expected to be a locator wrapping any found in + // the workspace (i.e. WorkspaceLocators). + private readonly workspace: ILocator, + ) { + super([...nonWorkspace, workspace]); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const iterators: IPythonEnvsIterator[] = [this.workspace.iterEnvs(query)]; + if (!query?.searchLocations?.doNotIncludeNonRooted) { + const nonWorkspace = query?.providerId + ? this.nonWorkspace.filter((locator) => query.providerId === locator.providerId) + : this.nonWorkspace; + iterators.push(...nonWorkspace.map((loc) => loc.iterEnvs(query))); + } + return combineIterators(iterators); + } +} +type WorkspaceLocatorFactoryResult = ILocator & Partial; +type WorkspaceLocatorFactory = (root: Uri) => WorkspaceLocatorFactoryResult[]; +type RootURI = string; + +export type WatchRootsArgs = { + initRoot(root: Uri): void; + addRoot(root: Uri): void; + removeRoot(root: Uri): void; +}; +type WatchRootsFunc = (args: WatchRootsArgs) => IDisposable; +// XXX Factor out RootedLocators and MultiRootedLocators. +/** + * The collection of all workspace-specific locators used by the extension. + * + * The factories are used to produce the locators for each workspace folder. + */ + +export class WorkspaceLocators extends LazyResourceBasedLocator { + public readonly providerId: string = 'workspace-locators'; + + private readonly locators: Record, IDisposable]> = {}; + + private readonly roots: Record = {}; + + constructor(private readonly watchRoots: WatchRootsFunc, private readonly factories: WorkspaceLocatorFactory[]) { + super(); + this.activate().ignoreErrors(); + } + + public async dispose(): Promise { + await super.dispose(); + + // Clear all the roots. + const roots = Object.keys(this.roots).map((key) => this.roots[key]); + roots.forEach((root) => this.removeRoot(root)); + } + + protected doIterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const iterators = Object.keys(this.locators).map((key) => { + if (query?.searchLocations !== undefined) { + const root = this.roots[key]; + // Match any related search location. + const filter = getURIFilter(root, { checkParent: true, checkChild: true }); + // Ignore any requests for global envs. + if (!query.searchLocations.roots.some(filter)) { + // This workspace folder did not match the query, so skip it! + return iterEmpty(); + } + if (query.providerId && query.providerId !== this.providerId) { + // This is a request for a specific provider, so skip it. + return iterEmpty(); + } + } + // The query matches or was not location-specific. + const [locator] = this.locators[key]; + return locator.iterEnvs(query); + }); + return combineIterators(iterators); + } + + protected async initResources(): Promise { + const disposable = this.watchRoots({ + initRoot: (root: Uri) => this.addRoot(root), + addRoot: (root: Uri) => { + // Drop the old one, if necessary. + this.removeRoot(root); + this.addRoot(root); + this.emitter.fire({ searchLocation: root }); + }, + removeRoot: (root: Uri) => { + this.removeRoot(root); + this.emitter.fire({ searchLocation: root }); + }, + }); + this.disposables.push(disposable); + } + + private addRoot(root: Uri): void { + // Create the root's locator, wrapping each factory-generated locator. + const locators: ILocator[] = []; + const disposables = new Disposables(); + this.factories.forEach((create) => { + create(root).forEach((loc) => { + locators.push(loc); + if (loc.dispose !== undefined) { + disposables.push(loc as IDisposable); + } + }); + }); + const locator = new Locators(locators); + // Cache it. + const key = root.toString(); + this.locators[key] = [locator, disposables]; + this.roots[key] = root; + // Hook up the watchers. + disposables.push( + locator.onChanged((e) => { + if (e.searchLocation === undefined) { + e.searchLocation = root; + } + this.emitter.fire(e); + }), + ); + } + + private removeRoot(root: Uri): void { + const key = root.toString(); + const found = this.locators[key]; + if (found === undefined) { + return; + } + const [, disposables] = found; + delete this.locators[key]; + delete this.roots[key]; + disposables.dispose(); + } +} diff --git a/src/client/pythonEnvironments/base/watcher.ts b/src/client/pythonEnvironments/base/watcher.ts new file mode 100644 index 000000000000..a9d0ef65595e --- /dev/null +++ b/src/client/pythonEnvironments/base/watcher.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, EventEmitter, Uri } from 'vscode'; +import { FileChangeType } from '../../common/platform/fileSystemWatcher'; +import { PythonEnvInfo, 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; + type?: FileChangeType; +}; + +/** + * The full set of possible info for a Python environments event. + */ +export type PythonEnvsChangedEvent = BasicPythonEnvsChangedEvent & { + /** + * The location, if any, affected by the event. + */ + searchLocation?: Uri; + /** + * A specific provider, if any, affected by the event. + */ + providerId?: string; + /** + * The env, if any, affected by the event. + */ + envPath?: string; +}; + +export type PythonEnvCollectionChangedEvent = BasicPythonEnvCollectionChangedEvent & { + type?: FileChangeType; + searchLocation?: Uri; +}; + +export type BasicPythonEnvCollectionChangedEvent = { + old?: PythonEnvInfo; + new?: PythonEnvInfo | undefined; +}; + +/** + * 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): void { + 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..60bf5f7516da --- /dev/null +++ b/src/client/pythonEnvironments/base/watchers.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event } from 'vscode'; +import { IDisposable } from '../../common/types'; +import { Disposables } from '../../common/utils/resourceLifecycle'; +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, IDisposable { + public readonly onChanged: Event; + + private readonly watcher = new PythonEnvsWatcher(); + + private readonly disposables = new Disposables(); + + constructor(watchers: ReadonlyArray) { + this.onChanged = this.watcher.onChanged; + watchers.forEach((w) => { + const disposable = w.onChanged((e) => this.watcher.fire(e)); + this.disposables.push(disposable); + }); + } + + public async dispose(): Promise { + await this.disposables.dispose(); + } +} diff --git a/src/client/pythonEnvironments/common/commonUtils.ts b/src/client/pythonEnvironments/common/commonUtils.ts new file mode 100644 index 000000000000..4bd94e0402ab --- /dev/null +++ b/src/client/pythonEnvironments/common/commonUtils.ts @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as path from 'path'; +import { convertFileType, DirEntry, FileType, getFileFilter, getFileType } from '../../common/utils/filesystem'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { traceError, traceVerbose } from '../../logging'; +import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../base/info'; +import { comparePythonVersionSpecificity } from '../base/info/env'; +import { parseVersion } from '../base/info/pythonVersion'; +import { getPythonVersionFromConda } from './environmentManagers/conda'; +import { getPythonVersionFromPyvenvCfg } from './environmentManagers/simplevirtualenvs'; +import { isFile, normCasePath } from './externalDependencies'; +import * as posix from './posixUtils'; +import * as windows from './windowsUtils'; + +const matchStandardPythonBinFilename = + getOSType() === OSType.Windows ? windows.matchPythonBinFilename : posix.matchPythonBinFilename; +type FileFilterFunc = (filename: string) => boolean; + +/** + * Returns `true` if path provided is likely a python executable than a folder path. + */ +export async function isPythonExecutable(filePath: string): Promise { + const isMatch = matchStandardPythonBinFilename(filePath); + if (isMatch && getOSType() === OSType.Windows) { + // On Windows it's fair to assume a path ending with `.exe` denotes a file. + return true; + } + if (await isFile(filePath)) { + return true; + } + return false; +} + +/** + * Searches recursively under the given `root` directory for python interpreters. + * @param root : Directory where the search begins. + * @param recurseLevels : Number of levels to search for from the root directory. + * @param filter : Callback that identifies directories to ignore. + */ +export async function* findInterpretersInDir( + root: string, + recurseLevel?: number, + filterSubDir?: FileFilterFunc, + ignoreErrors = true, +): AsyncIterableIterator { + // "checkBin" is a local variable rather than global + // so we can stub out getOSType() during unit testing. + const checkBin = getOSType() === OSType.Windows ? windows.matchPythonBinFilename : posix.matchPythonBinFilename; + const cfg = { + ignoreErrors, + filterSubDir, + filterFile: checkBin, + // Make no-recursion the default for backward compatibility. + maxDepth: recurseLevel || 0, + }; + // We use an initial depth of 1. + for await (const entry of walkSubTree(root, 1, cfg)) { + const { filename, filetype } = entry; + if (filetype === FileType.File || filetype === FileType.SymbolicLink) { + if (matchFile(filename, checkBin, ignoreErrors)) { + yield entry; + } + } + // We ignore all other file types. + } +} + +/** + * Find all Python executables in the given directory. + */ +export async function* iterPythonExecutablesInDir( + dirname: string, + opts: { + ignoreErrors: boolean; + } = { ignoreErrors: true }, +): AsyncIterableIterator { + const readDirOpts = { + ...opts, + filterFile: matchStandardPythonBinFilename, + }; + const entries = await readDirEntries(dirname, readDirOpts); + for (const entry of entries) { + const { filetype } = entry; + if (filetype === FileType.File || filetype === FileType.SymbolicLink) { + yield entry; + } + // We ignore all other file types. + } +} + +// This function helps simplify the recursion case. +async function* walkSubTree( + subRoot: string, + // "currentDepth" is the depth of the current level of recursion. + currentDepth: number, + cfg: { + filterSubDir: FileFilterFunc | undefined; + maxDepth: number; + ignoreErrors: boolean; + }, +): AsyncIterableIterator { + const entries = await readDirEntries(subRoot, cfg); + for (const entry of entries) { + yield entry; + + const { filename, filetype } = entry; + if (filetype === FileType.Directory) { + if (cfg.maxDepth < 0 || currentDepth <= cfg.maxDepth) { + if (matchFile(filename, cfg.filterSubDir, cfg.ignoreErrors)) { + yield* walkSubTree(filename, currentDepth + 1, cfg); + } + } + } + } +} + +async function readDirEntries( + dirname: string, + opts: { + filterFilename?: FileFilterFunc; + ignoreErrors: boolean; + } = { ignoreErrors: true }, +): Promise { + const ignoreErrors = opts.ignoreErrors || false; + if (opts.filterFilename && getOSType() === OSType.Windows) { + // Since `readdir()` using "withFileTypes" is not efficient + // on Windows, we take advantage of the filter. + let basenames: string[]; + try { + basenames = await fs.promises.readdir(dirname); + } catch (err) { + const exception = err as NodeJS.ErrnoException; + // Treat a missing directory as empty. + if (exception.code === 'ENOENT') { + return []; + } + if (ignoreErrors) { + traceError(`readdir() failed for "${dirname}" (${err})`); + return []; + } + throw err; // re-throw + } + const filenames = basenames + .map((b) => path.join(dirname, b)) + .filter((f) => matchFile(f, opts.filterFilename, ignoreErrors)); + return Promise.all( + filenames.map(async (filename) => { + const filetype = (await getFileType(filename, opts)) || FileType.Unknown; + return { filename, filetype }; + }), + ); + } + + let raw: fs.Dirent[]; + try { + raw = await fs.promises.readdir(dirname, { withFileTypes: true }); + } catch (err) { + const exception = err as NodeJS.ErrnoException; + // Treat a missing directory as empty. + if (exception.code === 'ENOENT') { + return []; + } + if (ignoreErrors) { + traceError(`readdir() failed for "${dirname}" (${err})`); + return []; + } + throw err; // re-throw + } + // (FYI) + // Normally we would have to do an extra (expensive) `fs.lstat()` + // here for each file to determine its file type. However, we + // avoid this by using the "withFileTypes" option to `readdir()` + // above. On non-Windows the file type of each entry is preserved + // for free. Unfortunately, on Windows it actually does an + // `lstat()` under the hood, so it isn't a win. Regardless, + // if we needed more information than just the file type + // then we would be forced to incur the extra cost + // of `lstat()` anyway. + const entries = raw.map((entry) => { + const filename = path.join(dirname, entry.name); + const filetype = convertFileType(entry); + return { filename, filetype }; + }); + if (opts.filterFilename) { + return entries.filter((e) => matchFile(e.filename, opts.filterFilename, ignoreErrors)); + } + return entries; +} + +function matchFile( + filename: string, + filterFile: FileFilterFunc | undefined, + // If "ignoreErrors" is true then we treat a failed filter + // as though it returned `false`. + ignoreErrors = true, +): boolean { + if (filterFile === undefined) { + return true; + } + try { + return filterFile(filename); + } catch (err) { + if (ignoreErrors) { + traceError(`filter failed for "${filename}" (${err})`); + return false; + } + throw err; // re-throw + } +} + +/** + * Looks for files in the same directory which might have version in their name. + * @param interpreterPath + */ +async function getPythonVersionFromNearByFiles(interpreterPath: string): Promise { + const root = path.dirname(interpreterPath); + let version = UNKNOWN_PYTHON_VERSION; + for await (const entry of findInterpretersInDir(root)) { + const { filename } = entry; + try { + const curVersion = parseVersion(path.basename(filename)); + if (comparePythonVersionSpecificity(curVersion, version) > 0) { + version = curVersion; + } + } catch (ex) { + // Ignore any parse errors + } + } + return version; +} + +/** + * This function does the best effort of finding version of python without running the + * python binary. + * @param interpreterPath Absolute path to the interpreter. + * @param hint Any string that might contain version info. + */ +export async function getPythonVersionFromPath(interpreterPath: string, hint?: string): Promise { + let versionA; + try { + versionA = hint ? parseVersion(hint) : UNKNOWN_PYTHON_VERSION; + } catch (ex) { + versionA = UNKNOWN_PYTHON_VERSION; + } + const versionB = interpreterPath ? await getPythonVersionFromNearByFiles(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version B for', interpreterPath, JSON.stringify(versionB)); + const versionC = interpreterPath ? await getPythonVersionFromPyvenvCfg(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version C for', interpreterPath, JSON.stringify(versionC)); + const versionD = interpreterPath ? await getPythonVersionFromConda(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version D for', interpreterPath, JSON.stringify(versionD)); + + let version = UNKNOWN_PYTHON_VERSION; + for (const v of [versionA, versionB, versionC, versionD]) { + version = comparePythonVersionSpecificity(version, v) > 0 ? version : v; + } + return version; +} + +/** + * Decide if the file is meets the given criteria for a Python executable. + */ +async function checkPythonExecutable( + executable: string | DirEntry, + opts: { + matchFilename?: (f: string) => boolean; + filterFile?: (f: string | DirEntry) => Promise; + }, +): Promise { + const matchFilename = opts.matchFilename || matchStandardPythonBinFilename; + const filename = typeof executable === 'string' ? executable : executable.filename; + + if (!matchFilename(filename)) { + return false; + } + + // This should occur after we match file names. This is to avoid doing potential + // `lstat` calls on too many files which can slow things down. + if (opts.filterFile && !(await opts.filterFile(executable))) { + return false; + } + + // For some use cases it would also be a good idea to verify that + // the file is executable. That is a relatively expensive operation + // (a stat on linux and actually executing the file on Windows), so + // at best it should be an optional check. If we went down this + // route then it would be worth supporting `fs.Stats` as a type + // for the "executable" arg. + // + // Regardless, currently there is no code that would use such + // an option, so for now we don't bother supporting it. + + return true; +} + +const filterGlobalExecutable = getFileFilter({ ignoreFileType: FileType.SymbolicLink })!; + +/** + * Decide if the file is a typical Python executable. + * + * This is a best effort operation with a focus on the common cases + * and on efficiency. The filename must be basic (python/python.exe). + * For global envs, symlinks are ignored. + */ +export async function looksLikeBasicGlobalPython(executable: string | DirEntry): Promise { + // "matchBasic" is a local variable rather than global + // so we can stub out getOSType() during unit testing. + const matchBasic = + getOSType() === OSType.Windows ? windows.matchBasicPythonBinFilename : posix.matchBasicPythonBinFilename; + + // We could be more permissive here by using matchPythonBinFilename(). + // Originally one key motivation for the "basic" check was to avoid + // symlinks (which often look like python3.exe, etc., particularly + // on Windows). However, the symbolic link check here eliminates + // that rationale to an extent. + // (See: https://github.com/microsoft/vscode-python/issues/15447) + const matchFilename = matchBasic; + const filterFile = filterGlobalExecutable; + return checkPythonExecutable(executable, { matchFilename, filterFile }); +} + +/** + * Decide if the file is a typical Python executable. + * + * This is a best effort operation with a focus on the common cases + * and on efficiency. The filename must be basic (python/python.exe). + * For global envs, symlinks are ignored. + */ +export async function looksLikeBasicVirtualPython(executable: string | DirEntry): Promise { + // "matchBasic" is a local variable rather than global + // so we can stub out getOSType() during unit testing. + const matchBasic = + getOSType() === OSType.Windows ? windows.matchBasicPythonBinFilename : posix.matchBasicPythonBinFilename; + + // With virtual environments, we match only the simplest name + // (e.g. `python`) and we do not ignore symlinks. + const matchFilename = matchBasic; + const filterFile = undefined; + return checkPythonExecutable(executable, { matchFilename, filterFile }); +} + +/** + * This function looks specifically for 'python' or 'python.exe' binary in the sub folders of a given + * environment directory. + * @param envDir Absolute path to the environment directory + */ +export async function getInterpreterPathFromDir( + envDir: string, + opts: { + global?: boolean; + ignoreErrors?: boolean; + } = {}, +): Promise { + const recurseLevel = 2; + + // Ignore any folders or files that not directly python binary related. + function filterDir(dirname: string): boolean { + const lower = path.basename(dirname).toLowerCase(); + return ['bin', 'scripts'].includes(lower); + } + + // Search in the sub-directories for python binary + const matchExecutable = opts.global ? looksLikeBasicGlobalPython : looksLikeBasicVirtualPython; + const executables = findInterpretersInDir(envDir, recurseLevel, filterDir, opts.ignoreErrors); + for await (const entry of executables) { + if (await matchExecutable(entry)) { + return entry.filename; + } + } + return undefined; +} + +/** + * Gets the root environment directory based on the absolute path to the python + * interpreter binary. + * @param interpreterPath Absolute path to the python interpreter + */ +export function getEnvironmentDirFromPath(interpreterPath: string): string { + const skipDirs = ['bin', 'scripts']; + + // env <--- Return this directory if it is not 'bin' or 'scripts' + // |__ python <--- interpreterPath + const dir = path.basename(path.dirname(interpreterPath)); + if (!skipDirs.map((e) => normCasePath(e)).includes(normCasePath(dir))) { + return path.dirname(interpreterPath); + } + + // This is the best next guess. + // env <--- Return this directory if it is not 'bin' or 'scripts' + // |__ bin or Scripts + // |__ python <--- interpreterPath + return path.dirname(path.dirname(interpreterPath)); +} diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts new file mode 100644 index 000000000000..89ff84823673 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceWarn } from '../../logging'; +import { PythonEnvKind } from '../base/info'; +import { getPrioritizedEnvKinds } from '../base/info/envKind'; +import { isCondaEnvironment } from './environmentManagers/conda'; +import { isGloballyInstalledEnv } from './environmentManagers/globalInstalledEnvs'; +import { isPipenvEnvironment } from './environmentManagers/pipenv'; +import { isPoetryEnvironment } from './environmentManagers/poetry'; +import { isPyenvEnvironment } from './environmentManagers/pyenv'; +import { + isVenvEnvironment, + isVirtualenvEnvironment as isVirtualEnvEnvironment, + isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment, +} from './environmentManagers/simplevirtualenvs'; +import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv'; +import { isActiveStateEnvironment } from './environmentManagers/activestate'; +import { isPixiEnvironment } from './environmentManagers/pixi'; + +const notImplemented = () => Promise.resolve(false); + +function getIdentifiers(): Map Promise> { + const defaultTrue = () => Promise.resolve(true); + const identifier: Map Promise> = new Map(); + Object.values(PythonEnvKind).forEach((k) => { + identifier.set(k, notImplemented); + }); + + identifier.set(PythonEnvKind.Conda, isCondaEnvironment); + identifier.set(PythonEnvKind.MicrosoftStore, isMicrosoftStoreEnvironment); + identifier.set(PythonEnvKind.Pipenv, isPipenvEnvironment); + identifier.set(PythonEnvKind.Pyenv, isPyenvEnvironment); + identifier.set(PythonEnvKind.Poetry, isPoetryEnvironment); + identifier.set(PythonEnvKind.Pixi, isPixiEnvironment); + identifier.set(PythonEnvKind.Venv, isVenvEnvironment); + identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); + identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); + identifier.set(PythonEnvKind.ActiveState, isActiveStateEnvironment); + identifier.set(PythonEnvKind.Unknown, defaultTrue); + identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv); + return identifier; +} + +export function isIdentifierRegistered(kind: PythonEnvKind): boolean { + const identifiers = getIdentifiers(); + const identifier = identifiers.get(kind); + if (identifier === notImplemented) { + return false; + } + return true; +} + +/** + * Returns environment type. + * @param {string} path : Absolute path to the python interpreter binary or path to environment. + * @returns {PythonEnvKind} + */ +export async function identifyEnvironment(path: string): Promise { + const identifiers = getIdentifiers(); + const prioritizedEnvTypes = getPrioritizedEnvKinds(); + for (const e of prioritizedEnvTypes) { + const identifier = identifiers.get(e); + if ( + identifier && + (await identifier(path).catch((ex) => { + traceWarn(`Identifier for ${e} failed to identify ${path}`, ex); + return false; + })) + ) { + return e; + } + } + return PythonEnvKind.Unknown; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts new file mode 100644 index 000000000000..5f22a96e4f83 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { dirname } from 'path'; +import { + arePathsSame, + getPythonSetting, + onDidChangePythonSetting, + pathExists, + shellExecute, +} from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceError, traceVerbose } from '../../../logging'; +import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; + +export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath'; + +const STATE_GENERAL_TIMEOUT = 5000; + +export type ProjectInfo = { + name: string; + organization: string; + local_checkouts: string[]; // eslint-disable-line camelcase + executables: string[]; +}; + +export async function isActiveStateEnvironment(interpreterPath: string): Promise { + const execDir = path.dirname(interpreterPath); + const runtimeDir = path.dirname(execDir); + return pathExists(path.join(runtimeDir, '_runtime_store')); +} + +export class ActiveState { + private static statePromise: Promise | undefined; + + public static async getState(): Promise { + if (ActiveState.statePromise === undefined) { + ActiveState.statePromise = ActiveState.locate(); + } + return ActiveState.statePromise; + } + + constructor() { + onDidChangePythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY, () => { + ActiveState.statePromise = undefined; + }); + } + + public static getStateToolDir(): string | undefined { + const home = getUserHomeDir(); + if (!home) { + return undefined; + } + return getOSType() === OSType.Windows + ? path.join(home, 'AppData', 'Local', 'ActiveState', 'StateTool') + : path.join(home, '.local', 'ActiveState', 'StateTool'); + } + + private static async locate(): Promise { + const stateToolDir = this.getStateToolDir(); + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) { + return new ActiveState(); + } + return undefined; + } + + public async getProjects(): Promise { + return this.getProjectsCached(); + } + + private static readonly defaultStateCommand: string = 'state'; + + // eslint-disable-next-line class-methods-use-this + @cache(30_000, true, 10_000) + private async getProjectsCached(): Promise { + try { + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + const result = await shellExecute(`${stateCommand} projects -o editor`, { + timeout: STATE_GENERAL_TIMEOUT, + }); + if (!result) { + return undefined; + } + let output = result.stdout.trimEnd(); + if (output[output.length - 1] === '\0') { + // '\0' is a record separator. + output = output.substring(0, output.length - 1); + } + traceVerbose(`${stateCommand} projects -o editor: ${output}`); + const projects = JSON.parse(output); + ActiveState.setCachedProjectInfo(projects); + return projects; + } catch (ex) { + traceError(ex); + return undefined; + } + } + + // Stored copy of known projects. isActiveStateEnvironmentForWorkspace() is + // not async, so getProjects() cannot be used. ActiveStateLocator sets this + // when it resolves project info. + private static cachedProjectInfo: ProjectInfo[] = []; + + public static getCachedProjectInfo(): ProjectInfo[] { + return this.cachedProjectInfo; + } + + private static setCachedProjectInfo(projects: ProjectInfo[]): void { + this.cachedProjectInfo = projects; + } +} + +export function isActiveStateEnvironmentForWorkspace(interpreterPath: string, workspacePath: string): boolean { + const interpreterDir = dirname(interpreterPath); + for (const project of ActiveState.getCachedProjectInfo()) { + if (project.executables) { + for (const [i, dir] of project.executables.entries()) { + // Note multiple checkouts for the same interpreter may exist. + // Check them all. + if (arePathsSame(dir, interpreterDir) && arePathsSame(workspacePath, project.local_checkouts[i])) { + return true; + } + } + } + } + return false; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts new file mode 100644 index 000000000000..c1bfd7d68bc2 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -0,0 +1,647 @@ +import * as path from 'path'; +import { lt, SemVer } from 'semver'; +import * as fsapi from '../../../common/platform/fs-paths'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { + arePathsSame, + getPythonSetting, + isParentPath, + pathExists, + readFile, + onDidChangePythonSetting, + exec, +} from '../externalDependencies'; + +import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../base/info'; +import { parseVersion } from '../../base/info/pythonVersion'; + +import { getRegistryInterpreters } from '../windowsUtils'; +import { EnvironmentType, PythonEnvironment } from '../../info'; +import { cache } from '../../../common/utils/decorators'; +import { isTestExecution } from '../../../common/constants'; +import { traceError, traceVerbose } from '../../../logging'; +import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { splitLines } from '../../../common/stringUtils'; +import { SpawnOptions } from '../../../common/process/types'; +import { sleep } from '../../../common/utils/async'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; + +export const AnacondaCompanyName = 'Anaconda, Inc.'; +export const CONDAPATH_SETTING_KEY = 'condaPath'; +export type CondaEnvironmentInfo = { + name: string; + path: string; +}; + +// This type corresponds to the output of "conda info --json", and property +// names must be spelled exactly as they are in order to match the schema. +export type CondaInfo = { + envs?: string[]; + envs_dirs?: string[]; // eslint-disable-line camelcase + 'sys.version'?: string; + 'sys.prefix'?: string; + python_version?: string; // eslint-disable-line camelcase + default_prefix?: string; // eslint-disable-line camelcase + root_prefix?: string; // eslint-disable-line camelcase + conda_version?: string; // eslint-disable-line camelcase + conda_shlvl?: number; // eslint-disable-line camelcase + config_files?: string[]; // eslint-disable-line camelcase + rc_path?: string; // eslint-disable-line camelcase + sys_rc_path?: string; // eslint-disable-line camelcase + user_rc_path?: string; // eslint-disable-line camelcase +}; + +type CondaEnvInfo = { + prefix: string; + name?: string; +}; + +/** + * Return the list of conda env interpreters. + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +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 undefined; + } + const details = await getPythonInfo(pythonPath); + if (!details) { + return undefined; + } + + 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)) + + .then((interpreters) => interpreters.map((interpreter) => interpreter!)); +} + +export function getCondaMetaPaths(interpreterPathOrEnvPath: string): string[] { + 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(interpreterPathOrEnvPath), 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(interpreterPathOrEnvPath)), condaMetaDir); + + const condaEnvDir3 = path.join(interpreterPathOrEnvPath, condaMetaDir); + + // The paths are ordered in the most common to least common + return [condaEnvDir1, condaEnvDir2, condaEnvDir3]; +} + +/** + * Checks if the given interpreter path belongs to a conda environment. Using + * known folder layout, and presence of 'conda-meta' directory. + * @param {string} interpreterPathOrEnvPath: 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" + * ] + * } + */ +export async function isCondaEnvironment(interpreterPathOrEnvPath: string): Promise { + const condaMetaPaths = getCondaMetaPaths(interpreterPathOrEnvPath); + // We don't need to test all at once, testing each one here + for (const condaMeta of condaMetaPaths) { + if (await pathExists(condaMeta)) { + return true; + } + } + return false; +} + +/** + * Gets path to conda's `environments.txt` file. More info https://github.com/conda/conda/issues/11845. + */ +export async function getCondaEnvironmentsTxt(): Promise { + const homeDir = getUserHomeDir(); + if (!homeDir) { + return []; + } + const environmentsTxt = path.join(homeDir, '.conda', 'environments.txt'); + return [environmentsTxt]; +} + +/** + * Extracts version information from `conda-meta/history` near a given interpreter. + * @param interpreterPath Absolute path to the interpreter + * + * Remarks: This function looks for `conda-meta/history` usually in the same or parent directory. + * Reads the `conda-meta/history` and finds the line that contains 'python-3.9.0`. Gets the + * version string from that lines and parses it. + */ +export async function getPythonVersionFromConda(interpreterPath: string): Promise { + const configPaths = getCondaMetaPaths(interpreterPath).map((p) => path.join(p, 'history')); + const pattern = /\:python-(([\d\.a-z]?)+)/; + + // We want to check each of those locations in the order. There is no need to look at + // all of them in parallel. + for (const configPath of configPaths) { + if (await pathExists(configPath)) { + try { + const lines = splitLines(await readFile(configPath)); + + // Sample data: + // +defaults/linux-64::pip-20.2.4-py38_0 + // +defaults/linux-64::python-3.8.5-h7579374_1 + // +defaults/linux-64::readline-8.0-h7b6447c_0 + const pythonVersionStrings = lines + .map((line) => { + // Here we should have only lines with 'python-' in it. + // +defaults/linux-64::python-3.8.5-h7579374_1 + + const matches = pattern.exec(line); + // Typically there will be 3 matches + // 0: "python-3.8.5" + // 1: "3.8.5" + // 2: "5" + + // we only need the second one + return matches ? matches[1] : ''; + }) + .filter((v) => v.length > 0); + + if (pythonVersionStrings.length > 0) { + const last = pythonVersionStrings.length - 1; + return parseVersion(pythonVersionStrings[last].trim()); + } + } catch (ex) { + // There is usually only one `conda-meta/history`. If we found, it but + // failed to parse it, then just return here. No need to look for versions + // any further. + return UNKNOWN_PYTHON_VERSION; + } + } + } + + return UNKNOWN_PYTHON_VERSION; +} + +/** + * Return the interpreter's filename for the given environment. + */ +export function getCondaInterpreterPath(condaEnvironmentPath: string): string { + // where to find the Python binary within a conda env. + const relativePath = getOSType() === OSType.Windows ? 'python.exe' : path.join('bin', 'python'); + const filePath = path.join(condaEnvironmentPath, relativePath); + return filePath; +} + +// Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. +export const CONDA_RUN_VERSION = '4.9.0'; +export const CONDA_ACTIVATION_TIMEOUT = 45000; +const CONDA_GENERAL_TIMEOUT = 45000; + +/** Wraps the "conda" utility, and exposes its functionality. + */ +export class Conda { + /** + * Locating conda binary is expensive, since it potentially involves spawning or + * trying to spawn processes; so it's done lazily and asynchronously. Methods that + * need a Conda instance should use getConda() to obtain it, and should never access + * this property directly. + */ + private static condaPromise = new Map>(); + + private condaInfoCached = new Map | undefined>(); + + /** + * Carries path to conda binary to be used for shell execution. + */ + public readonly shellCommand: string; + + /** + * Creates a Conda service corresponding to the corresponding "conda" command. + * + * @param command - Command used to spawn conda. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + */ + constructor( + readonly command: string, + shellCommand?: string, + private readonly shellPath?: string, + private readonly useWorkerThreads?: boolean, + ) { + if (this.useWorkerThreads === undefined) { + this.useWorkerThreads = false; + } + this.shellCommand = shellCommand ?? command; + onDidChangePythonSetting(CONDAPATH_SETTING_KEY, () => { + Conda.condaPromise = new Map>(); + }); + } + + public static async getConda(shellPath?: string): Promise { + if (Conda.condaPromise.get(shellPath) === undefined || isTestExecution()) { + Conda.condaPromise.set(shellPath, Conda.locate(shellPath)); + } + return Conda.condaPromise.get(shellPath); + } + + public static setConda(condaPath: string): void { + Conda.condaPromise.set(undefined, Promise.resolve(new Conda(condaPath))); + } + + /** + * Locates the preferred "conda" utility on this system by considering user settings, + * binaries on PATH, Python interpreters in the registry, and known install locations. + * + * @return A Conda instance corresponding to the binary, if successful; otherwise, undefined. + */ + private static async locate(shellPath?: string): Promise { + traceVerbose(`Searching for conda.`); + const home = getUserHomeDir(); + let customCondaPath: string | undefined = 'conda'; + try { + customCondaPath = getPythonSetting(CONDAPATH_SETTING_KEY); + } catch (ex) { + traceError(`Failed to get conda path setting, ${ex}`); + } + const suffix = getOSType() === OSType.Windows ? 'Scripts\\conda.exe' : 'bin/conda'; + + // Produce a list of candidate binaries to be probed by exec'ing them. + async function* getCandidates() { + if (customCondaPath && customCondaPath !== 'conda') { + // If user has specified a custom conda path, use it first. + yield customCondaPath; + } + // Check unqualified filename first, in case it's on PATH. + yield 'conda'; + if (getOSType() === OSType.Windows) { + yield* getCandidatesFromRegistry(); + } + yield* getCandidatesFromKnownPaths(); + yield* getCandidatesFromEnvironmentsTxt(); + } + + async function* getCandidatesFromRegistry() { + const interps = await getRegistryInterpreters(); + const candidates = interps + .filter((interp) => interp.interpreterPath && interp.distroOrgName === 'ContinuumAnalytics') + .map((interp) => path.join(path.win32.dirname(interp.interpreterPath), suffix)); + yield* candidates; + } + + async function* getCandidatesFromKnownPaths() { + // Check common locations. We want to look up "/*conda*/", where prefix and suffix + // depend on the platform, to account for both Anaconda and Miniconda, and all possible variations. + // The check cannot use globs, because on Windows, prefixes are absolute paths with a drive letter, + // and the glob module doesn't understand globs with drive letters in them, producing wrong results + // for "C:/*" etc. + const prefixes: string[] = []; + if (getOSType() === OSType.Windows) { + const programData = getEnvironmentVariable('PROGRAMDATA') || 'C:\\ProgramData'; + prefixes.push(programData); + if (home) { + const localAppData = getEnvironmentVariable('LOCALAPPDATA') || path.join(home, 'AppData', 'Local'); + prefixes.push(home, path.join(localAppData, 'Continuum')); + } + } else { + prefixes.push('/usr/share', '/usr/local/share', '/opt', '/opt/homebrew/bin'); + if (home) { + prefixes.push(home, path.join(home, 'opt')); + } + } + + for (const prefix of prefixes) { + let items: string[] | undefined; + try { + items = await fsapi.readdir(prefix); + } catch (ex) { + // Directory doesn't exist or is not readable - not an error. + items = undefined; + } + if (items !== undefined) { + yield* items + .filter((fileName) => fileName.toLowerCase().includes('conda')) + .map((fileName) => path.join(prefix, fileName, suffix)); + } + } + } + + async function* getCandidatesFromEnvironmentsTxt() { + if (!home) { + return; + } + + let contents: string; + try { + contents = await fsapi.readFile(path.join(home, '.conda', 'environments.txt'), 'utf8'); + } catch (ex) { + // File doesn't exist or is not readable - not an error. + contents = ''; + } + + // Match conda behavior; see conda.gateways.disk.read.yield_lines(). + // Note that this precludes otherwise legal paths with trailing spaces. + yield* contents + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line !== '' && !line.startsWith('#')) + .map((line) => path.join(line, suffix)); + } + + async function getCondaBatFile(file: string) { + const fileDir = path.dirname(file); + const possibleBatch = path.join(fileDir, '..', 'condabin', 'conda.bat'); + if (await pathExists(possibleBatch)) { + return possibleBatch; + } + return undefined; + } + + // Probe the candidates, and pick the first one that exists and does what we need. + for await (const condaPath of getCandidates()) { + traceVerbose(`Probing conda binary: ${condaPath}`); + let conda = new Conda(condaPath, undefined, shellPath); + try { + await conda.getInfo(); + if (getOSType() === OSType.Windows && (isTestExecution() || condaPath !== customCondaPath)) { + // Prefer to use .bat files over .exe on windows as that is what cmd works best on. + // Do not translate to `.bat` file if the setting explicitly sets the executable. + const condaBatFile = await getCondaBatFile(condaPath); + try { + if (condaBatFile) { + const condaBat = new Conda(condaBatFile, undefined, shellPath); + await condaBat.getInfo(); + conda = new Conda(condaPath, condaBatFile, shellPath); + } + } catch (ex) { + traceVerbose('Failed to spawn conda bat file', condaBatFile, ex); + } + } + traceVerbose(`Found conda via filesystem probing: ${condaPath}`); + return conda; + } catch (ex) { + // Failed to spawn because the binary doesn't exist or isn't on PATH, or the current + // user doesn't have execute permissions for it, or this conda couldn't handle command + // line arguments that we passed (indicating an old version that we do not support). + traceVerbose('Failed to spawn conda binary', condaPath, ex); + } + } + + // Didn't find anything. + traceVerbose("Couldn't locate the conda binary."); + return undefined; + } + + /** + * Retrieves global information about this conda. + * Corresponds to "conda info --json". + */ + public async getInfo(useCache?: boolean): Promise { + let condaInfoCached = this.condaInfoCached.get(this.shellPath); + if (!useCache || !condaInfoCached) { + condaInfoCached = this.getInfoImpl(this.command, this.shellPath); + this.condaInfoCached.set(this.shellPath, condaInfoCached); + } + return condaInfoCached; + } + + /** + * Temporarily cache result for this particular command. + */ + @cache(30_000, true, 10_000) + // eslint-disable-next-line class-methods-use-this + private async getInfoImpl(command: string, shellPath: string | undefined): Promise { + const options: SpawnOptions = { timeout: CONDA_GENERAL_TIMEOUT }; + if (shellPath) { + options.shell = shellPath; + } + const resultPromise = exec(command, ['info', '--json'], options, this.useWorkerThreads); + // It has been observed that specifying a timeout is still not reliable to terminate the Conda process, see #27915. + // Hence explicitly continue execution after timeout has been reached. + const success = await Promise.race([ + resultPromise.then(() => true), + sleep(CONDA_GENERAL_TIMEOUT + 3000).then(() => false), + ]); + if (success) { + const result = await resultPromise; + traceVerbose(`${command} info --json: ${result.stdout}`); + return JSON.parse(result.stdout); + } + throw new Error(`Launching '${command} info --json' timed out`); + } + + /** + * Retrieves list of Python environments known to this conda. + * Corresponds to "conda env list --json", but also computes environment names. + */ + @cache(30_000, true, 10_000) + public async getEnvList(): Promise { + const info = await this.getInfo(); + const { envs } = info; + if (envs === undefined) { + return []; + } + return Promise.all( + envs.map(async (prefix) => ({ + prefix, + name: await this.getName(prefix, info), + })), + ); + } + + /** + * Retrieves list of directories where conda environments are stored. + */ + @cache(30_000, true, 10_000) + public async getEnvDirs(): Promise { + const info = await this.getInfo(); + return info.envs_dirs ?? []; + } + + public async getName(prefix: string, info?: CondaInfo): Promise { + info = info ?? (await this.getInfo(true)); + if (info.root_prefix && arePathsSame(prefix, info.root_prefix)) { + return 'base'; + } + const parentDir = path.dirname(prefix); + if (info.envs_dirs !== undefined) { + for (const envsDir of info.envs_dirs) { + if (arePathsSame(parentDir, envsDir)) { + return path.basename(prefix); + } + } + } + return undefined; + } + + /** + * Returns conda environment related to path provided. + * @param executableOrEnvPath Path to environment folder or path to interpreter that uniquely identifies an environment. + */ + public async getCondaEnvironment(executableOrEnvPath: string): Promise { + const envList = await this.getEnvList(); + // Assuming `executableOrEnvPath` is path to env. + const condaEnv = envList.find((e) => arePathsSame(executableOrEnvPath, e.prefix)); + if (condaEnv) { + return condaEnv; + } + // Assuming `executableOrEnvPath` is an executable. + return envList.find((e) => isParentPath(executableOrEnvPath, e.prefix)); + } + + /** + * Returns executable associated with the conda env, swallows exceptions. + */ + // eslint-disable-next-line class-methods-use-this + public async getInterpreterPathForEnvironment(condaEnv: CondaEnvInfo | { prefix: string }): Promise { + const executablePath = getCondaInterpreterPath(condaEnv.prefix); + if (await pathExists(executablePath)) { + traceVerbose('Found executable within conda env', JSON.stringify(condaEnv)); + return executablePath; + } + traceVerbose( + 'Executable does not exist within conda env, assume the executable to be `python`', + JSON.stringify(condaEnv), + ); + return 'python'; + } + + public async getRunPythonArgs( + env: CondaEnvInfo, + forShellExecution?: boolean, + isolatedFlag = false, + ): Promise { + const condaVersion = await this.getCondaVersion(); + if (condaVersion && lt(condaVersion, CONDA_RUN_VERSION)) { + traceError('`conda run` is not supported for conda version', condaVersion.raw); + return undefined; + } + const args = []; + args.push('-p', env.prefix); + + const python = [ + forShellExecution ? this.shellCommand : this.command, + 'run', + ...args, + '--no-capture-output', + 'python', + ]; + if (isolatedFlag) { + python.push('-I'); + } + return [...python, OUTPUT_MARKER_SCRIPT]; + } + + public async getListPythonPackagesArgs( + env: CondaEnvInfo, + forShellExecution?: boolean, + ): Promise { + const args = ['-p', env.prefix]; + + return [forShellExecution ? this.shellCommand : this.command, 'list', ...args]; + } + + /** + * Return the conda version. The version info is cached. + */ + @cache(-1, true) + public async getCondaVersion(): Promise { + const info = await this.getInfo(true).catch(() => undefined); + let versionString: string | undefined; + if (info && info.conda_version) { + versionString = info.conda_version; + } else { + const stdOut = await exec(this.command, ['--version'], { timeout: CONDA_GENERAL_TIMEOUT }) + .then((result) => result.stdout.trim()) + .catch(() => undefined); + + versionString = stdOut && stdOut.startsWith('conda ') ? stdOut.substring('conda '.length).trim() : stdOut; + } + if (!versionString) { + return undefined; + } + const pattern = /(?\d+)\.(?\d+)\.(?\d+)(?:.*)?/; + const match = versionString.match(pattern); + if (match && match.groups) { + const versionStringParsed = match.groups.major.concat('.', match.groups.minor, '.', match.groups.micro); + + const semVarVersion: SemVer = new SemVer(versionStringParsed); + if (semVarVersion) { + return semVarVersion; + } + } + // Use a bogus version, at least to indicate the fact that a version was returned. + // This ensures we still use conda for activation, installation etc. + traceError(`Unable to parse version of Conda, ${versionString}`); + return new SemVer('0.0.1'); + } + + public async isCondaRunSupported(): Promise { + const condaVersion = await this.getCondaVersion(); + if (condaVersion && lt(condaVersion, CONDA_RUN_VERSION)) { + return false; + } + return true; + } +} + +export function setCondaBinary(executable: string): void { + Conda.setConda(executable); +} + +export async function getCondaEnvDirs(): Promise { + const conda = await Conda.getConda(); + return conda?.getEnvDirs(); +} + +export function getCondaPathSetting(): string | undefined { + const config = getConfiguration('python'); + return config.get(CONDAPATH_SETTING_KEY, ''); +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts new file mode 100644 index 000000000000..0aa91bdbfb45 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts @@ -0,0 +1,158 @@ +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { traceVerbose } from '../../../logging'; +import { cache } from '../../../common/utils/decorators'; +import { ICondaService } from '../../../interpreter/contracts'; +import { traceDecoratorVerbose } from '../../../logging'; +import { Conda, CondaEnvironmentInfo, CondaInfo } from './conda'; + +/** + * Injectable version of Conda utility. + */ +@injectable() +export class CondaService implements ICondaService { + private isAvailable: boolean | undefined; + + constructor( + @inject(IPlatformService) private platform: IPlatformService, + @inject(IFileSystem) private fileSystem: IFileSystem, + ) {} + + public async getActivationScriptFromInterpreter( + interpreterPath?: string, + envName?: string, + ): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined> { + traceVerbose(`Getting activation script for interpreter ${interpreterPath}, env ${envName}`); + const condaPath = await this.getCondaFileFromInterpreter(interpreterPath, envName); + traceVerbose(`Found conda path: ${condaPath}`); + + const activatePath = (condaPath + ? path.join(path.dirname(condaPath), 'activate') + : 'activate' + ).fileToCommandArgumentForPythonExt(); // maybe global activate? + traceVerbose(`Using activate path: ${activatePath}`); + + // try to find the activate script in the global conda root prefix. + if (this.platform.isLinux || this.platform.isMac) { + const condaInfo = await this.getCondaInfo(); + // eslint-disable-next-line camelcase + if (condaInfo?.root_prefix) { + const globalActivatePath = path + // eslint-disable-next-line camelcase + .join(condaInfo.root_prefix, this.platform.virtualEnvBinName, 'activate') + .fileToCommandArgumentForPythonExt(); + + if (activatePath === globalActivatePath || !(await this.fileSystem.fileExists(activatePath))) { + traceVerbose(`Using global activate path: ${globalActivatePath}`); + return { + path: globalActivatePath, + type: 'global', + }; + } + } + } + + return { path: activatePath, type: 'local' }; // return the default activate script wether it exists or not. + } + + /** + * Return the path to the "conda file". + */ + + // eslint-disable-next-line class-methods-use-this + public async getCondaFile(forShellExecution?: boolean): Promise { + return Conda.getConda().then((conda) => { + const command = forShellExecution ? conda?.shellCommand : conda?.command; + return command ?? 'conda'; + }); + } + + // eslint-disable-next-line class-methods-use-this + public async getInterpreterPathForEnvironment(condaEnv: CondaEnvironmentInfo): Promise { + const conda = await Conda.getConda(); + return conda?.getInterpreterPathForEnvironment({ name: condaEnv.name, prefix: condaEnv.path }); + } + + /** + * 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)) // eslint-disable-line no-return-assign + .catch(() => (this.isAvailable = false)); // eslint-disable-line no-return-assign + } + + /** + * Return the conda version. + */ + // eslint-disable-next-line class-methods-use-this + public async getCondaVersion(): Promise { + return Conda.getConda().then((conda) => conda?.getCondaVersion()); + } + + /** + * 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. + */ + @traceDecoratorVerbose('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; + } + + return this.getCondaFile(); + } + + /** + * Return the info reported by the conda install. + * The result is cached for 30s. + */ + + // eslint-disable-next-line class-methods-use-this + @cache(60_000) + public async getCondaInfo(): Promise { + const conda = await Conda.getConda(); + return conda?.getInfo(); + } +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/globalInstalledEnvs.ts b/src/client/pythonEnvironments/common/environmentManagers/globalInstalledEnvs.ts new file mode 100644 index 000000000000..eb52668a0c65 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/globalInstalledEnvs.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getSearchPathEntries } from '../../../common/utils/exec'; +import { getOSType, OSType } from '../../../common/utils/platform'; +import { isParentPath } from '../externalDependencies'; +import { commonPosixBinPaths } from '../posixUtils'; +import { isPyenvShimDir } from './pyenv'; + +/** + * Checks if the given interpreter belongs to known globally installed types. If an global + * executable is discoverable, we consider it as global type. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isGloballyInstalledEnv(executablePath: string): Promise { + // Identifying this type is not important, as the extension treats `Global` and `Unknown` + // types the same way. This is only required for telemetry. As windows registry is known + // to be slow, we do not want to unnecessarily block on that by default, hence skip this + // step. + // if (getOSType() === OSType.Windows) { + // if (await isFoundInWindowsRegistry(executablePath)) { + // return true; + // } + // } + return isFoundInPathEnvVar(executablePath); +} + +async function isFoundInPathEnvVar(executablePath: string): Promise { + let searchPathEntries: string[] = []; + if (getOSType() === OSType.Windows) { + searchPathEntries = getSearchPathEntries(); + } else { + searchPathEntries = await commonPosixBinPaths(); + } + // Filter out pyenv shims. They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + searchPathEntries = searchPathEntries.filter((dirname) => !isPyenvShimDir(dirname)); + for (const searchPath of searchPathEntries) { + if (isParentPath(executablePath, searchPath)) { + return true; + } + } + return false; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts new file mode 100644 index 000000000000..6d7a13ea1557 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts @@ -0,0 +1,116 @@ +import { isTestExecution } from '../../../common/constants'; +import { exec, pathExists } from '../externalDependencies'; +import { traceVerbose } from '../../../logging'; +import { cache } from '../../../common/utils/decorators'; +import { getOSType, OSType } from '../../../common/utils/platform'; + +/** Wraps the "Hatch" utility, and exposes its functionality. + */ +export class Hatch { + /** + * Locating Hatch binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ + private static hatchPromise: Map> = new Map< + string, + Promise + >(); + + /** + * Creates a Hatch service corresponding to the corresponding "hatch" command. + * + * @param command - Command used to run hatch. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + * @param cwd - The working directory to use as cwd when running hatch. + */ + constructor(public readonly command: string, private cwd: string) { + this.fixCwd(); + } + + /** + * Returns a Hatch instance corresponding to the binary which can be used to run commands for the cwd. + * + * Every directory is a valid Hatch project, so this should always return a Hatch instance. + */ + public static async getHatch(cwd: string): Promise { + if (Hatch.hatchPromise.get(cwd) === undefined || isTestExecution()) { + Hatch.hatchPromise.set(cwd, Hatch.locate(cwd)); + } + return Hatch.hatchPromise.get(cwd); + } + + private static async locate(cwd: string): Promise { + // First thing this method awaits on should be hatch command execution, + // hence perform all operations before that synchronously. + const hatchPath = 'hatch'; + traceVerbose(`Probing Hatch binary ${hatchPath}`); + const hatch = new Hatch(hatchPath, cwd); + const virtualenvs = await hatch.getEnvList(); + if (virtualenvs !== undefined) { + traceVerbose(`Found hatch binary ${hatchPath}`); + return hatch; + } + traceVerbose(`Failed to find Hatch binary ${hatchPath}`); + + // Didn't find anything. + traceVerbose(`No Hatch binary found`); + return undefined; + } + + /** + * Retrieves list of Python environments known to Hatch for this working directory. + * Returns `undefined` if we failed to spawn in some way. + * + * Corresponds to "hatch env show --json". Swallows errors if any. + */ + public async getEnvList(): Promise { + return this.getEnvListCached(this.cwd); + } + + /** + * Method created to facilitate caching. The caching decorator uses function arguments as cache key, + * so pass in cwd on which we need to cache. + */ + @cache(30_000, true, 10_000) + private async getEnvListCached(_cwd: string): Promise { + const envInfoOutput = await exec(this.command, ['env', 'show', '--json'], { + cwd: this.cwd, + throwOnStdErr: true, + }).catch(traceVerbose); + if (!envInfoOutput) { + return undefined; + } + const envPaths = await Promise.all( + Object.keys(JSON.parse(envInfoOutput.stdout)).map(async (name) => { + const envPathOutput = await exec(this.command, ['env', 'find', name], { + cwd: this.cwd, + throwOnStdErr: true, + }).catch(traceVerbose); + if (!envPathOutput) return undefined; + const dir = envPathOutput.stdout.trim(); + return (await pathExists(dir)) ? dir : undefined; + }), + ); + return envPaths.flatMap((r) => (r ? [r] : [])); + } + + /** + * Due to an upstream hatch issue on Windows https://github.com/pypa/hatch/issues/1350, + * 'hatch env find default' does not handle case-insensitive paths as cwd, which are valid on Windows. + * So we need to pass the case-exact path as cwd. + * It has been observed that only the drive letter in `cwd` is lowercased here. Unfortunately, + * there's no good way to get case of the drive letter correctly without using Win32 APIs: + * https://stackoverflow.com/questions/33086985/how-to-obtain-case-exact-path-of-a-file-in-node-js-on-windows + * So we do it manually. + */ + private fixCwd(): void { + if (getOSType() === OSType.Windows) { + if (/^[a-z]:/.test(this.cwd)) { + // Replace first character by the upper case version of the character. + const a = this.cwd.split(':'); + a[0] = a[0].toUpperCase(); + this.cwd = a.join(':'); + } + } + } +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/macDefault.ts b/src/client/pythonEnvironments/common/environmentManagers/macDefault.ts new file mode 100644 index 000000000000..931fbbba9eac --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/macDefault.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getOSType, OSType } from '../../../common/utils/platform'; + +/** + * Decide if the given Python executable looks like the MacOS default Python. + */ +export function isMacDefaultPythonPath(pythonPath: string): boolean { + if (getOSType() !== OSType.OSX) { + return false; + } + + const defaultPaths = ['/usr/bin/python']; + + return defaultPaths.includes(pythonPath) || pythonPath.startsWith('/usr/bin/python2'); +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.ts b/src/client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.ts new file mode 100644 index 000000000000..2b8675d0bc0b --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.ts @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { getEnvironmentVariable } from '../../../common/utils/platform'; +import { traceWarn } from '../../../logging'; +import { pathExists } from '../externalDependencies'; + +/** + * Gets path to the Windows Apps directory. + * @returns {string} : Returns path to the Windows Apps directory under + * `%LOCALAPPDATA%/Microsoft/WindowsApps`. + */ +export function getMicrosoftStoreAppsRoot(): string { + const localAppData = getEnvironmentVariable('LOCALAPPDATA') || ''; + return path.join(localAppData, 'Microsoft', 'WindowsApps'); +} +/** + * Checks if a given path is under the forbidden microsoft store directory. + * @param {string} absPath : Absolute path to a file or directory. + * @returns {boolean} : Returns true if `interpreterPath` is under + * `%ProgramFiles%/WindowsApps`. + */ +function isForbiddenStorePath(absPath: string): boolean { + const programFilesStorePath = path + .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') + .normalize() + .toUpperCase(); + return path.normalize(absPath).toUpperCase().includes(programFilesStorePath); +} +/** + * Checks if a given directory is any one of the possible microsoft store directories, or + * its sub-directory. + * @param {string} dirPath : Absolute path to a directory. + * + * Remarks: + * These locations are tested: + * 1. %LOCALAPPDATA%/Microsoft/WindowsApps + * 2. %ProgramFiles%/WindowsApps + */ + +export function isMicrosoftStoreDir(dirPath: string): boolean { + const storeRootPath = path.normalize(getMicrosoftStoreAppsRoot()).toUpperCase(); + return path.normalize(dirPath).toUpperCase().includes(storeRootPath) || isForbiddenStorePath(dirPath); +} +/** + * Checks if store python is installed. + * @param {string} interpreterPath : Absolute path to a interpreter. + * Remarks: + * If store python was never installed then the store apps directory will not + * have idle.exe or pip.exe. We can use this as a way to identify the python.exe + * found in the store apps directory is a real python or a store install shortcut. + */ +export async function isStorePythonInstalled(interpreterPath?: string): Promise { + let results = await Promise.all([ + pathExists(path.join(getMicrosoftStoreAppsRoot(), 'idle.exe')), + pathExists(path.join(getMicrosoftStoreAppsRoot(), 'pip.exe')), + ]); + + if (results.includes(true)) { + return true; + } + + if (interpreterPath) { + results = await Promise.all([ + pathExists(path.join(path.dirname(interpreterPath), 'idle.exe')), + pathExists(path.join(path.dirname(interpreterPath), 'pip.exe')), + ]); + return results.includes(true); + } + return false; +} +/** + * Checks if the given interpreter belongs to Microsoft 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, Microsoft 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 isMicrosoftStoreEnvironment(interpreterPath: string): Promise { + if (await isStorePythonInstalled(interpreterPath)) { + const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); + const localAppDataStorePath = path.normalize(getMicrosoftStoreAppsRoot()).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)) { + traceWarn('isMicrosoftStoreEnvironment called with Program Files store path.'); + return true; + } + } + return false; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts b/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts new file mode 100644 index 000000000000..c8651533ed4c --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { getEnvironmentVariable } from '../../../common/utils/platform'; +import { traceError, traceVerbose } from '../../../logging'; +import { arePathsSame, normCasePath, pathExists, readFile } from '../externalDependencies'; + +function getSearchHeight() { + // PIPENV_MAX_DEPTH tells pipenv the maximum number of directories to recursively search for + // a Pipfile, defaults to 3: https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_MAX_DEPTH + const maxDepthStr = getEnvironmentVariable('PIPENV_MAX_DEPTH'); + if (maxDepthStr === undefined) { + return 3; + } + const maxDepth = parseInt(maxDepthStr, 10); + // eslint-disable-next-line no-restricted-globals + if (isNaN(maxDepth)) { + traceError(`PIPENV_MAX_DEPTH is incorrectly set. Converting value '${maxDepthStr}' to number results in NaN`); + return 1; + } + return maxDepth; +} + +/** + * Returns the path to Pipfile associated with the provided directory. + * @param searchDir the directory to look into + * @param lookIntoParentDirectories set to true if we should also search for Pipfile in parent directory + */ +export async function _getAssociatedPipfile( + searchDir: string, + options: { lookIntoParentDirectories: boolean }, +): Promise { + const pipFileName = getEnvironmentVariable('PIPENV_PIPFILE') || 'Pipfile'; + let heightToSearch = options.lookIntoParentDirectories ? getSearchHeight() : 1; + while (heightToSearch > 0 && !arePathsSame(searchDir, path.dirname(searchDir))) { + const pipFile = path.join(searchDir, pipFileName); + if (await pathExists(pipFile)) { + return pipFile; + } + searchDir = path.dirname(searchDir); + heightToSearch -= 1; + } + return undefined; +} + +/** + * If interpreter path belongs to a pipenv environment which is located inside a project, return associated Pipfile, + * otherwise return `undefined`. + * @param interpreterPath Absolute path to any python interpreter. + */ +async function getPipfileIfLocal(interpreterPath: string): Promise { + // Local pipenv environments are created by setting PIPENV_VENV_IN_PROJECT to 1, which always names the environment + // folder '.venv': https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_VENV_IN_PROJECT + // This is the layout we wish to verify. + // project + // |__ Pipfile <--- check if Pipfile exists here + // |__ .venv <--- check if name of the folder is '.venv' + // |__ Scripts/bin + // |__ python <--- interpreterPath + const venvFolder = path.dirname(path.dirname(interpreterPath)); + if (path.basename(venvFolder) !== '.venv') { + return undefined; + } + const directoryWhereVenvResides = path.dirname(venvFolder); + return _getAssociatedPipfile(directoryWhereVenvResides, { lookIntoParentDirectories: false }); +} + +/** + * Returns the project directory for pipenv environments given the environment folder + * @param envFolder Path to the environment folder + */ +export async function getProjectDir(envFolder: string): Promise { + // Global pipenv environments have a .project file with the absolute path to the project + // See https://github.com/pypa/pipenv/blob/v2018.6.25/CHANGELOG.rst#features--improvements + // This is the layout we expect + // + // |__ .project <--- check if .project exists here + // |__ Scripts/bin + // |__ python <--- interpreterPath + // We get the project by reading the .project file + const dotProjectFile = path.join(envFolder, '.project'); + if (!(await pathExists(dotProjectFile))) { + return undefined; + } + const projectDir = (await readFile(dotProjectFile)).trim(); + if (!(await pathExists(projectDir))) { + traceVerbose( + `The .project file inside environment folder: ${envFolder} doesn't contain a valid path to the project`, + ); + return undefined; + } + return projectDir; +} + +/** + * If interpreter path belongs to a global pipenv environment, return associated Pipfile, otherwise return `undefined`. + * @param interpreterPath Absolute path to any python interpreter. + */ +async function getPipfileIfGlobal(interpreterPath: string): Promise { + const envFolder = path.dirname(path.dirname(interpreterPath)); + const projectDir = await getProjectDir(envFolder); + if (projectDir === undefined) { + return undefined; + } + + // This is the layout we expect to see. + // project + // |__ Pipfile <--- check if Pipfile exists here and return it + // The name of the project (directory where Pipfile resides) is used as a prefix in the environment folder + const envFolderName = path.basename(normCasePath(envFolder)); + if (!envFolderName.startsWith(`${path.basename(normCasePath(projectDir))}-`)) { + return undefined; + } + + return _getAssociatedPipfile(projectDir, { lookIntoParentDirectories: false }); +} + +/** + * Checks if the given interpreter path belongs to a pipenv environment, by locating the Pipfile which was used to + * create the environment. + * @param interpreterPath: Absolute path to any python interpreter. + */ +export async function isPipenvEnvironment(interpreterPath: string): Promise { + if (await getPipfileIfLocal(interpreterPath)) { + return true; + } + if (await getPipfileIfGlobal(interpreterPath)) { + return true; + } + return false; +} + +/** + * Returns true if interpreter path belongs to a global pipenv environment which is associated with a particular folder, + * false otherwise. + * @param interpreterPath Absolute path to any python interpreter. + */ +export async function isPipenvEnvironmentRelatedToFolder(interpreterPath: string, folder: string): Promise { + const pipFileAssociatedWithEnvironment = await getPipfileIfGlobal(interpreterPath); + if (!pipFileAssociatedWithEnvironment) { + return false; + } + + // PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories + // https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT + const lookIntoParentDirectories = getEnvironmentVariable('PIPENV_NO_INHERIT') === undefined; + const pipFileAssociatedWithFolder = await _getAssociatedPipfile(folder, { lookIntoParentDirectories }); + if (!pipFileAssociatedWithFolder) { + return false; + } + return arePathsSame(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder); +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/pixi.ts b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts new file mode 100644 index 000000000000..6443e64f9ae8 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { readJSON } from 'fs-extra'; +import which from 'which'; +import { getUserHomeDir, isWindows } from '../../../common/utils/platform'; +import { exec, getPythonSetting, onDidChangePythonSetting, pathExists } from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceVerbose, traceWarn } from '../../../logging'; +import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { IDisposableRegistry } from '../../../common/types'; +import { getWorkspaceFolderPaths } from '../../../common/vscodeApis/workspaceApis'; +import { isTestExecution } from '../../../common/constants'; +import { TerminalShellType } from '../../../common/terminal/types'; + +export const PIXITOOLPATH_SETTING_KEY = 'pixiToolPath'; + +// This type corresponds to the output of 'pixi info --json', and property +// names must be spelled exactly as they are in order to match the schema. +export type PixiInfo = { + platform: string; + virtual_packages: string[]; // eslint-disable-line camelcase + version: string; + cache_dir: string; // eslint-disable-line camelcase + cache_size?: number; // eslint-disable-line camelcase + auth_dir: string; // eslint-disable-line camelcase + + project_info?: PixiProjectInfo /* eslint-disable-line camelcase */; + + environments_info: /* eslint-disable-line camelcase */ { + name: string; + features: string[]; + solve_group: string; // eslint-disable-line camelcase + environment_size: number; // eslint-disable-line camelcase + dependencies: string[]; + tasks: string[]; + channels: string[]; + prefix: string; + }[]; +}; + +export type PixiProjectInfo = { + manifest_path: string; // eslint-disable-line camelcase + last_updated: string; // eslint-disable-line camelcase + pixi_folder_size?: number; // eslint-disable-line camelcase + version: string; +}; + +export type PixiEnvMetadata = { + manifest_path: string; // eslint-disable-line camelcase + pixi_version: string; // eslint-disable-line camelcase + environment_name: string; // eslint-disable-line camelcase +}; + +export async function isPixiEnvironment(interpreterPath: string): Promise { + const prefix = getPrefixFromInterpreterPath(interpreterPath); + return ( + pathExists(path.join(prefix, 'conda-meta/pixi')) || pathExists(path.join(prefix, 'conda-meta/pixi_env_prefix')) + ); +} + +/** + * Returns the path to the environment directory based on the interpreter path. + */ +export function getPrefixFromInterpreterPath(interpreterPath: string): string { + const interpreterDir = path.dirname(interpreterPath); + if (!interpreterDir.endsWith('bin') && !interpreterDir.endsWith('Scripts')) { + return interpreterDir; + } + return path.dirname(interpreterDir); +} + +async function findPixiOnPath(): Promise { + try { + return await which('pixi', { all: true }); + } catch { + // Ignore errors + } + return []; +} + +/** Wraps the "pixi" utility, and exposes its functionality. + */ +export class Pixi { + /** + * Creates a Pixi service corresponding to the corresponding "pixi" command. + * + * @param command - Command used to run pixi. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + */ + constructor(public readonly command: string) {} + + /** + * Retrieves list of Python environments known to this pixi for the specified directory. + * + * Corresponds to "pixi info --json" and extracting the environments. Swallows errors if any. + */ + public async getEnvList(cwd: string): Promise { + const pixiInfo = await this.getPixiInfo(cwd); + // eslint-disable-next-line camelcase + return pixiInfo?.environments_info.map((env) => env.prefix); + } + + /** + * Method that runs `pixi info` and returns the result. The value is cached for "only" 1 second + * because the output changes if the project manifest is modified. + */ + @cache(1_000, true, 1_000) + public async getPixiInfo(cwd: string): Promise { + try { + const infoOutput = await exec(this.command, ['info', '--json'], { + cwd, + throwOnStdErr: false, + }); + + if (!infoOutput || !infoOutput.stdout) { + return undefined; + } + + const pixiInfo: PixiInfo = JSON.parse(infoOutput.stdout); + return pixiInfo; + } catch (error) { + traceWarn(`Failed to get pixi info for ${cwd}`, error); + return undefined; + } + } + + /** + * Returns the command line arguments to run `python` within a specific pixi environment. + * @param manifestPath The path to the manifest file used by pixi. + * @param envName The name of the environment in the pixi project + * @param isolatedFlag Whether to add `-I` to the python invocation. + * @returns A list of arguments that can be passed to exec. + */ + public getRunPythonArgs(manifestPath: string, envName?: string, isolatedFlag = false): string[] { + let python = [this.command, 'run', '--manifest-path', manifestPath]; + if (isNonDefaultPixiEnvironmentName(envName)) { + python = python.concat(['--environment', envName]); + } + + python.push('python'); + if (isolatedFlag) { + python.push('-I'); + } + return [...python, OUTPUT_MARKER_SCRIPT]; + } + + /** + * Starting from Pixi 0.24.0, each environment has a special file that records some information + * about which manifest created the environment. + * + * @param envDir The root directory (or prefix) of a conda environment + */ + + // eslint-disable-next-line class-methods-use-this + @cache(5_000, true, 10_000) + async getPixiEnvironmentMetadata(envDir: string): Promise { + const pixiPath = path.join(envDir, 'conda-meta/pixi'); + try { + const result: PixiEnvMetadata | undefined = await readJSON(pixiPath); + return result; + } catch (e) { + traceVerbose(`Failed to get pixi environment metadata for ${envDir}`, e); + } + return undefined; + } +} + +async function getPixiTool(): Promise { + let pixi = getPythonSetting(PIXITOOLPATH_SETTING_KEY); + + if (!pixi || pixi === 'pixi' || !(await pathExists(pixi))) { + pixi = undefined; + const paths = await findPixiOnPath(); + for (const p of paths) { + if (await pathExists(p)) { + pixi = p; + break; + } + } + } + + if (!pixi) { + // Check the default installation location + const home = getUserHomeDir(); + if (home) { + const pixiToolPath = path.join(home, '.pixi', 'bin', isWindows() ? 'pixi.exe' : 'pixi'); + if (await pathExists(pixiToolPath)) { + pixi = pixiToolPath; + } + } + } + + return pixi ? new Pixi(pixi) : undefined; +} + +/** + * Locating pixi binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ +let _pixi: Promise | undefined; + +/** + * Returns a Pixi instance corresponding to the binary which can be used to run commands for the cwd. + * + * Pixi commands can be slow and so can be bottleneck to overall discovery time. So trigger command + * execution as soon as possible. To do that we need to ensure the operations before the command are + * performed synchronously. + */ +export function getPixi(): Promise { + if (_pixi === undefined || isTestExecution()) { + _pixi = getPixiTool(); + } + return _pixi; +} + +export type PixiEnvironmentInfo = { + interpreterPath: string; + pixi: Pixi; + pixiVersion: string; + manifestPath: string; + envName?: string; +}; + +function isPixiProjectDir(pixiProjectDir: string): boolean { + const paths = getWorkspaceFolderPaths().map((f) => path.normalize(f)); + const normalized = path.normalize(pixiProjectDir); + return paths.some((p) => p === normalized); +} + +/** + * Given the location of an interpreter, try to deduce information about the environment in which it + * resides. + * @param interpreterPath The full path to the interpreter. + * @param pixi Optionally a pixi instance. If this is not specified it will be located. + * @returns Information about the pixi environment. + */ +export async function getPixiEnvironmentFromInterpreter( + interpreterPath: string, +): Promise { + if (!interpreterPath) { + return undefined; + } + + const prefix = getPrefixFromInterpreterPath(interpreterPath); + const pixi = await getPixi(); + if (!pixi) { + traceVerbose(`could not find a pixi interpreter for the interpreter at ${interpreterPath}`); + return undefined; + } + + // Check if the environment has pixi metadata that we can source. + const metadata = await pixi.getPixiEnvironmentMetadata(prefix); + if (metadata !== undefined) { + return { + interpreterPath, + pixi, + pixiVersion: metadata.pixi_version, + manifestPath: metadata.manifest_path, + envName: metadata.environment_name, + }; + } + + // Otherwise, we'll have to try to deduce this information. + + // Usually the pixi environments are stored under `/.pixi/envs//`. So, + // we walk backwards to determine the project directory. + let envName: string | undefined; + let envsDir: string; + let dotPixiDir: string; + let pixiProjectDir: string; + let pixiInfo: PixiInfo | undefined; + + try { + envName = path.basename(prefix); + envsDir = path.dirname(prefix); + dotPixiDir = path.dirname(envsDir); + pixiProjectDir = path.dirname(dotPixiDir); + if (!isPixiProjectDir(pixiProjectDir)) { + traceVerbose(`could not determine the pixi project directory for the interpreter at ${interpreterPath}`); + return undefined; + } + + // Invoke pixi to get information about the pixi project + pixiInfo = await pixi.getPixiInfo(pixiProjectDir); + + if (!pixiInfo || !pixiInfo.project_info) { + traceWarn(`failed to determine pixi project information for the interpreter at ${interpreterPath}`); + return undefined; + } + + return { + interpreterPath, + pixi, + pixiVersion: pixiInfo.version, + manifestPath: pixiInfo.project_info.manifest_path, + envName, + }; + } catch (error) { + traceWarn('Error processing paths or getting Pixi Info:', error); + } + + return undefined; +} + +/** + * Returns true if the given environment name is *not* the default environment. + */ +export function isNonDefaultPixiEnvironmentName(envName?: string): envName is string { + return envName !== 'default'; +} + +export function registerPixiFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidChangePythonSetting(PIXITOOLPATH_SETTING_KEY, () => { + _pixi = getPixiTool(); + }), + ); +} + +/** + * Returns the `pixi run` command + */ +export async function getRunPixiPythonCommand(pythonPath: string): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'run', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + args.push('python'); + return args; +} + +export async function getPixiActivationCommands( + pythonPath: string, + _targetShell?: TerminalShellType, +): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'shell', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + // const pixiTargetShell = shellTypeToPixiShell(targetShell); + // if (pixiTargetShell) { + // args.push('--shell'); + // args.push(pixiTargetShell); + // } + + // const shellHookOutput = await exec(pixiEnv.pixi.command, args, { + // throwOnStdErr: false, + // }).catch(traceError); + // if (!shellHookOutput) { + // return undefined; + // } + + // return splitLines(shellHookOutput.stdout, { + // removeEmptyEntries: true, + // trim: true, + // }); + return [args.join(' ')]; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/poetry.ts b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts new file mode 100644 index 000000000000..5e5fa2416208 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { + getPythonSetting, + isParentPath, + pathExists, + pathExistsSync, + readFile, + shellExecute, +} from '../externalDependencies'; +import { getEnvironmentDirFromPath } from '../commonUtils'; +import { isVirtualenvEnvironment } from './simplevirtualenvs'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { cache } from '../../../common/utils/decorators'; +import { isTestExecution } from '../../../common/constants'; +import { traceError, traceVerbose } from '../../../logging'; +import { splitLines } from '../../../common/stringUtils'; + +/** + * Global virtual env dir for a project is named as: + * + * --py. + * + * Implementation details behind and are too + * much to rely upon, so for our purposes the best we can do is the following regex. + */ +const globalPoetryEnvDirRegex = /^(.+)-(.+)-py(\d).(\d){1,2}$/; + +/** + * Checks if the given interpreter belongs to a global poetry environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +async function isGlobalPoetryEnvironment(interpreterPath: string): Promise { + const envDir = getEnvironmentDirFromPath(interpreterPath); + return globalPoetryEnvDirRegex.test(path.basename(envDir)) ? isVirtualenvEnvironment(interpreterPath) : false; +} +/** + * Local poetry environments are created by the `virtualenvs.in-project` setting , which always names the environment + * folder '.venv': https://python-poetry.org/docs/configuration/#virtualenvsin-project-boolean + */ +export const localPoetryEnvDirName = '.venv'; + +/** + * Checks if the given interpreter belongs to a local poetry environment, i.e environment is located inside the project. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +async function isLocalPoetryEnvironment(interpreterPath: string): Promise { + // This is the layout we wish to verify. + // project + // |__ pyproject.toml <--- check if this exists + // |__ .venv <--- check if name of the folder is '.venv' + // |__ Scripts/bin + // |__ python <--- interpreterPath + const envDir = getEnvironmentDirFromPath(interpreterPath); + if (path.basename(envDir) !== localPoetryEnvDirName) { + return false; + } + const project = path.dirname(envDir); + if (!(await hasValidPyprojectToml(project))) { + return false; + } + // The assumption is that we need to be able to run poetry CLI for an environment in order to mark it as poetry. + // For that we can either further verify, + // - 'pyproject.toml' is valid toml + // - 'pyproject.toml' has a poetry section which contains the necessary fields + // - Poetry configuration allows local virtual environments + // ... possibly more + // Or we can try running poetry to find the related environment instead. Launching poetry binaries although + // reliable, can be expensive. So report the best effort type instead, i.e this is likely a poetry env. + return true; +} + +/** + * Checks if the given interpreter belongs to a poetry environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isPoetryEnvironment(interpreterPath: string): Promise { + if (await isGlobalPoetryEnvironment(interpreterPath)) { + return true; + } + if (await isLocalPoetryEnvironment(interpreterPath)) { + return true; + } + return false; +} + +const POETRY_TIMEOUT = 50000; + +/** Wraps the "poetry" utility, and exposes its functionality. + */ +export class Poetry { + /** + * Locating poetry binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ + private static poetryPromise: Map> = new Map< + string, + Promise + >(); + + /** + * Creates a Poetry service corresponding to the corresponding "poetry" command. + * + * @param command - Command used to run poetry. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + * @param cwd - The working directory to use as cwd when running poetry. + */ + constructor(public readonly command: string, private cwd: string) { + this.fixCwd(); + } + + /** + * Returns a Poetry instance corresponding to the binary which can be used to run commands for the cwd. + * + * Poetry commands can be slow and so can be bottleneck to overall discovery time. So trigger command + * execution as soon as possible. To do that we need to ensure the operations before the command are + * performed synchronously. + */ + public static async getPoetry(cwd: string): Promise { + // Following check should be performed synchronously so we trigger poetry execution as soon as possible. + if (!(await hasValidPyprojectToml(cwd))) { + // This check is not expensive and may change during a session, so we need not cache it. + return undefined; + } + if (Poetry.poetryPromise.get(cwd) === undefined || isTestExecution()) { + Poetry.poetryPromise.set(cwd, Poetry.locate(cwd)); + } + return Poetry.poetryPromise.get(cwd); + } + + private static async locate(cwd: string): Promise { + // First thing this method awaits on should be poetry command execution, hence perform all operations + // before that synchronously. + + traceVerbose(`Getting poetry for cwd ${cwd}`); + // Produce a list of candidate binaries to be probed by exec'ing them. + function* getCandidates() { + try { + const customPoetryPath = getPythonSetting('poetryPath'); + if (customPoetryPath && customPoetryPath !== 'poetry') { + // If user has specified a custom poetry path, use it first. + yield customPoetryPath; + } + } catch (ex) { + traceError(`Failed to get poetry setting`, ex); + } + // Check unqualified filename, in case it's on PATH. + yield 'poetry'; + const home = getUserHomeDir(); + if (home) { + const defaultPoetryPath = path.join(home, '.poetry', 'bin', 'poetry'); + if (pathExistsSync(defaultPoetryPath)) { + yield defaultPoetryPath; + } + } + } + + // Probe the candidates, and pick the first one that exists and does what we need. + for (const poetryPath of getCandidates()) { + traceVerbose(`Probing poetry binary for ${cwd}: ${poetryPath}`); + const poetry = new Poetry(poetryPath, cwd); + const virtualenvs = await poetry.getEnvList(); + if (virtualenvs !== undefined) { + traceVerbose(`Found poetry via filesystem probing for ${cwd}: ${poetryPath}`); + return poetry; + } + traceVerbose(`Failed to find poetry for ${cwd}: ${poetryPath}`); + } + + // Didn't find anything. + traceVerbose(`No poetry binary found for ${cwd}`); + return undefined; + } + + /** + * Retrieves list of Python environments known to this poetry for this working directory. + * Returns `undefined` if we failed to spawn because the binary doesn't exist or isn't on PATH, + * or the current user doesn't have execute permissions for it, or this poetry couldn't handle + * command line arguments that we passed (indicating an old version that we do not support, or + * poetry has not been setup properly for the cwd). + * + * Corresponds to "poetry env list --full-path". Swallows errors if any. + */ + public async getEnvList(): Promise { + return this.getEnvListCached(this.cwd); + } + + /** + * Method created to facilitate caching. The caching decorator uses function arguments as cache key, + * so pass in cwd on which we need to cache. + */ + @cache(30_000, true, 10_000) + private async getEnvListCached(_cwd: string): Promise { + const result = await this.safeShellExecute(`${this.command} env list --full-path`); + if (!result) { + return undefined; + } + /** + * We expect stdout to contain something like: + * + * \poetry_2-tutorial-project-6hnqYwvD-py3.7 + * \poetry_2-tutorial-project-6hnqYwvD-py3.8 + * \poetry_2-tutorial-project-6hnqYwvD-py3.9 (Activated) + * + * So we'll need to remove the string "(Activated)" after splitting lines to get the full path. + */ + const activated = '(Activated)'; + const res = await Promise.all( + splitLines(result.stdout).map(async (line) => { + if (line.endsWith(activated)) { + line = line.slice(0, -activated.length); + } + const folder = line.trim(); + return (await pathExists(folder)) ? folder : undefined; + }), + ); + return res.filter((r) => r !== undefined).map((r) => r!); + } + + /** + * Retrieves interpreter path of the currently activated virtual environment for this working directory. + * Corresponds to "poetry env info -p". Swallows errors if any. + */ + public async getActiveEnvPath(): Promise { + return this.getActiveEnvPathCached(this.cwd); + } + + /** + * Method created to facilitate caching. The caching decorator uses function arguments as cache key, + * so pass in cwd on which we need to cache. + */ + @cache(20_000, true, 10_000) + private async getActiveEnvPathCached(_cwd: string): Promise { + const result = await this.safeShellExecute(`${this.command} env info -p`, true); + if (!result) { + return undefined; + } + return result.stdout.trim(); + } + + /** + * Retrieves `virtualenvs.path` setting for this working directory. `virtualenvs.path` setting defines where virtual + * environments are created for the directory. Corresponds to "poetry config virtualenvs.path". Swallows errors if any. + */ + public async getVirtualenvsPathSetting(): Promise { + const result = await this.safeShellExecute(`${this.command} config virtualenvs.path`); + if (!result) { + return undefined; + } + return result.stdout.trim(); + } + + /** + * Due to an upstream poetry issue on Windows https://github.com/python-poetry/poetry/issues/3829, + * 'poetry env list' does not handle case-insensitive paths as cwd, which are valid on Windows. + * So we need to pass the case-exact path as cwd. + * It has been observed that only the drive letter in `cwd` is lowercased here. Unfortunately, + * there's no good way to get case of the drive letter correctly without using Win32 APIs: + * https://stackoverflow.com/questions/33086985/how-to-obtain-case-exact-path-of-a-file-in-node-js-on-windows + * So we do it manually. + */ + private fixCwd(): void { + if (getOSType() === OSType.Windows) { + if (/^[a-z]:/.test(this.cwd)) { + // Replace first character by the upper case version of the character. + const a = this.cwd.split(':'); + a[0] = a[0].toUpperCase(); + this.cwd = a.join(':'); + } + } + } + + private async safeShellExecute(command: string, logVerbose = false) { + // It has been observed that commands related to conda or poetry binary take upto 10-15 seconds unlike + // python binaries. So have a large timeout. + const stopWatch = new StopWatch(); + const result = await shellExecute(command, { + cwd: this.cwd, + throwOnStdErr: true, + timeout: POETRY_TIMEOUT, + }).catch((ex) => { + if (logVerbose) { + traceVerbose(ex); + } else { + traceError(ex); + } + return undefined; + }); + traceVerbose(`Time taken to run ${command} in ms`, stopWatch.elapsedTime); + return result; + } +} + +/** + * Returns true if interpreter path belongs to a poetry environment which is associated with a particular folder, + * false otherwise. + * @param interpreterPath Absolute path to any python interpreter. + * @param folder Absolute path to the folder. + * @param poetryPath Poetry command to use to calculate the result. + */ +export async function isPoetryEnvironmentRelatedToFolder( + interpreterPath: string, + folder: string, + poetryPath?: string, +): Promise { + const poetry = poetryPath ? new Poetry(poetryPath, folder) : await Poetry.getPoetry(folder); + const pathToEnv = await poetry?.getActiveEnvPath(); + if (!pathToEnv) { + return false; + } + return isParentPath(interpreterPath, pathToEnv); +} + +/** + * Does best effort to verify whether a folder has been setup for poetry, by looking for "valid" pyproject.toml file. + * Note "valid" is best effort here, i.e we only verify the minimal features. + * + * @param folder Folder to look for pyproject.toml file in. + */ +async function hasValidPyprojectToml(folder: string): Promise { + const pyprojectToml = path.join(folder, 'pyproject.toml'); + if (!pathExistsSync(pyprojectToml)) { + return false; + } + const content = await readFile(pyprojectToml); + if (!content.includes('[tool.poetry]')) { + return false; + } + // It may still be the case that. + // - pyproject.toml is not a valid toml file + // - Some fields are not setup properly for poetry or are missing + // ... possibly more + // But we only wish to verify the minimal features. + return true; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts b/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts new file mode 100644 index 000000000000..8556e6f19f90 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts @@ -0,0 +1,264 @@ +import * as path from 'path'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { arePathsSame, isParentPath, pathExists, shellExecute } from '../externalDependencies'; +import { traceVerbose } from '../../../logging'; + +export function getPyenvDir(): string { + // Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix. + // They contain the path to pyenv's installation folder. + // If they don't exist, use the default path: ~/.pyenv/pyenv-win on Windows, ~/.pyenv on Unix. + // If the interpreter path starts with the path to the pyenv folder, then it is a pyenv environment. + // See https://github.com/pyenv/pyenv#locating-the-python-installation for general usage, + // And https://github.com/pyenv-win/pyenv-win for Windows specifics. + let pyenvDir = getEnvironmentVariable('PYENV_ROOT') ?? getEnvironmentVariable('PYENV'); + + if (!pyenvDir) { + const homeDir = getUserHomeDir() || ''; + pyenvDir = + getOSType() === OSType.Windows ? path.join(homeDir, '.pyenv', 'pyenv-win') : path.join(homeDir, '.pyenv'); + } + + return pyenvDir; +} + +let pyenvBinary: string | undefined; + +export function setPyEnvBinary(pyenvBin: string): void { + pyenvBinary = pyenvBin; +} + +async function getPyenvBinary(): Promise { + if (pyenvBinary && (await pathExists(pyenvBinary))) { + return pyenvBinary; + } + + const pyenvDir = getPyenvDir(); + const pyenvBin = path.join(pyenvDir, 'bin', 'pyenv'); + if (await pathExists(pyenvBin)) { + return pyenvBin; + } + return 'pyenv'; +} + +export async function getActivePyenvForDirectory(cwd: string): Promise { + const pyenvBin = await getPyenvBinary(); + try { + const pyenvInterpreterPath = await shellExecute(`${pyenvBin} which python`, { cwd }); + return pyenvInterpreterPath.stdout.trim(); + } catch (ex) { + traceVerbose(ex); + return undefined; + } +} + +export function getPyenvVersionsDir(): string { + return path.join(getPyenvDir(), 'versions'); +} + +/** + * Checks if a given directory path is same as `pyenv` shims path. This checks + * `~/.pyenv/shims` on posix and `~/.pyenv/pyenv-win/shims` on windows. + * @param {string} dirPath: Absolute path to any directory + * @returns {boolean}: Returns true if the patch is same as `pyenv` shims directory. + */ + +export function isPyenvShimDir(dirPath: string): boolean { + const shimPath = path.join(getPyenvDir(), 'shims'); + return arePathsSame(shimPath, dirPath) || arePathsSame(`${shimPath}${path.sep}`, dirPath); +} +/** + * Checks if the given interpreter belongs to a pyenv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean}: Returns true if the interpreter belongs to a pyenv environment. + */ + +export async function isPyenvEnvironment(interpreterPath: string): Promise { + const pathToCheck = interpreterPath; + const pyenvDir = getPyenvDir(); + + if (!(await pathExists(pyenvDir))) { + return false; + } + + return isParentPath(pathToCheck, pyenvDir); +} + +export interface IPyenvVersionStrings { + pythonVer?: string; + distro?: string; + distroVer?: string; +} +/** + * This function provides parsers for some of the common and known distributions + * supported by pyenv. To get the list of supported pyenv distributions, run + * `pyenv install --list` + * + * The parsers below were written based on the list obtained from pyenv version 1.2.21 + */ +function getKnownPyenvVersionParsers(): Map IPyenvVersionStrings | undefined> { + /** + * This function parses versions that are plain python versions. + * @param str string to parse + * + * Parses : + * 2.7.18 + * 3.9.0 + */ + function pythonOnly(str: string): IPyenvVersionStrings { + return { + pythonVer: str, + distro: undefined, + distroVer: undefined, + }; + } + + /** + * This function parses versions that are distro versions. + * @param str string to parse + * + * Examples: + * miniconda3-4.7.12 + * anaconda3-2020.07 + */ + function distroOnly(str: string): IPyenvVersionStrings | undefined { + const parts = str.split('-'); + if (parts.length === 3) { + return { + pythonVer: undefined, + distroVer: `${parts[1]}-${parts[2]}`, + distro: parts[0], + }; + } + + if (parts.length === 2) { + return { + pythonVer: undefined, + distroVer: parts[1], + distro: parts[0], + }; + } + + return { + pythonVer: undefined, + distroVer: undefined, + distro: str, + }; + } + + /** + * This function parser pypy environments supported by the pyenv install command + * @param str string to parse + * + * Examples: + * pypy-c-jit-latest + * pypy-c-nojit-latest + * pypy-dev + * pypy-stm-2.3 + * pypy-stm-2.5.1 + * pypy-1.5-src + * pypy-1.5 + * pypy3.5-5.7.1-beta-src + * pypy3.5-5.7.1-beta + * pypy3.5-5.8.0-src + * pypy3.5-5.8.0 + */ + function pypyParser(str: string): IPyenvVersionStrings | undefined { + const pattern = /[0-9\.]+/; + + const parts = str.split('-'); + const pythonVer = parts[0].search(pattern) > 0 ? parts[0].substr('pypy'.length) : undefined; + if (parts.length === 2) { + return { + pythonVer, + distroVer: parts[1], + distro: 'pypy', + }; + } + + if ( + parts.length === 3 && + (parts[2].startsWith('src') || + parts[2].startsWith('beta') || + parts[2].startsWith('alpha') || + parts[2].startsWith('win64')) + ) { + const part1 = parts[1].startsWith('v') ? parts[1].substr(1) : parts[1]; + return { + pythonVer, + distroVer: `${part1}-${parts[2]}`, + distro: 'pypy', + }; + } + + if (parts.length === 3 && parts[1] === 'stm') { + return { + pythonVer, + distroVer: parts[2], + distro: `${parts[0]}-${parts[1]}`, + }; + } + + if (parts.length === 4 && parts[1] === 'c') { + return { + pythonVer, + distroVer: parts[3], + distro: `pypy-${parts[1]}-${parts[2]}`, + }; + } + + if (parts.length === 4 && parts[3].startsWith('src')) { + return { + pythonVer, + distroVer: `${parts[1]}-${parts[2]}-${parts[3]}`, + distro: 'pypy', + }; + } + + return { + pythonVer, + distroVer: undefined, + distro: 'pypy', + }; + } + + const parsers: Map IPyenvVersionStrings | undefined> = new Map(); + parsers.set('activepython', distroOnly); + parsers.set('anaconda', distroOnly); + parsers.set('graalpython', distroOnly); + parsers.set('ironpython', distroOnly); + parsers.set('jython', distroOnly); + parsers.set('micropython', distroOnly); + parsers.set('miniconda', distroOnly); + parsers.set('miniforge', distroOnly); + parsers.set('pypy', pypyParser); + parsers.set('pyston', distroOnly); + parsers.set('stackless', distroOnly); + parsers.set('3', pythonOnly); + parsers.set('2', pythonOnly); + + return parsers; +} +/** + * This function parses the name of the commonly installed versions of pyenv based environments. + * @param str string to parse. + * + * Remarks: Depending on the environment, the name itself can contain distribution info like + * name and version. Sometimes it may also have python version as a part of the name. This function + * extracts the various strings. + */ + +export function parsePyenvVersion(str: string): IPyenvVersionStrings | undefined { + const allParsers = getKnownPyenvVersionParsers(); + const knownPrefixes = Array.from(allParsers.keys()); + + const parsers = knownPrefixes + .filter((k) => str.startsWith(k)) + .map((p) => allParsers.get(p)) + .filter((p) => p !== undefined); + + if (parsers.length > 0 && parsers[0]) { + return parsers[0](str); + } + + return undefined; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts new file mode 100644 index 000000000000..0ad24252f341 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as fsapi from '../../../common/platform/fs-paths'; +import '../../../common/extensions'; +import { splitLines } from '../../../common/stringUtils'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../base/info'; +import { comparePythonVersionSpecificity } from '../../base/info/env'; +import { parseBasicVersion, parseRelease, parseVersion } from '../../base/info/pythonVersion'; +import { isParentPath, pathExists, readFile } from '../externalDependencies'; + +function getPyvenvConfigPathsFrom(interpreterPath: string): string[] { + const pyvenvConfigFile = 'pyvenv.cfg'; + + // Check if the pyvenv.cfg file is in the parent directory relative to the interpreter. + // env + // |__ pyvenv.cfg <--- check if this file exists + // |__ bin or Scripts + // |__ python <--- interpreterPath + const venvPath1 = path.join(path.dirname(path.dirname(interpreterPath)), pyvenvConfigFile); + + // Check if the pyvenv.cfg file is in the directory as the interpreter. + // env + // |__ pyvenv.cfg <--- check if this file exists + // |__ python <--- interpreterPath + const venvPath2 = path.join(path.dirname(interpreterPath), pyvenvConfigFile); + + // The paths are ordered in the most common to least common + return [venvPath1, venvPath2]; +} + +/** + * Checks if the given interpreter is a virtual environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isVirtualEnvironment(interpreterPath: string): Promise { + return isVenvEnvironment(interpreterPath); +} + +/** + * Checks if the given interpreter belongs to a venv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isVenvEnvironment(interpreterPath: string): Promise { + const venvPaths = getPyvenvConfigPathsFrom(interpreterPath); + + // We don't need to test all at once, testing each one here + for (const venvPath of venvPaths) { + if (await pathExists(venvPath)) { + return true; + } + } + return false; +} + +/** + * Checks if the given interpreter belongs to a virtualenv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a virtualenv environment. + */ +export async function isVirtualenvEnvironment(interpreterPath: string): Promise { + // Check if there are any activate.* files in the same directory as the interpreter. + // + // env + // |__ activate, activate.* <--- check if any of these files exist + // |__ python <--- interpreterPath + const directory = path.dirname(interpreterPath); + const files = await fsapi.readdir(directory); + const regex = /^activate(\.([A-z]|\d)+)?$/i; + + return files.find((file) => regex.test(file)) !== undefined; +} + +async function getDefaultVirtualenvwrapperDir(): Promise { + const homeDir = getUserHomeDir() || ''; + + // In Windows, the default path for WORKON_HOME is %USERPROFILE%\Envs. + // If 'Envs' is not available we should default to '.virtualenvs'. Since that + // is also valid for windows. + if (getOSType() === OSType.Windows) { + // ~/Envs with uppercase 'E' is the default home dir for + // virtualEnvWrapper. + const envs = path.join(homeDir, 'Envs'); + if (await pathExists(envs)) { + return envs; + } + } + return path.join(homeDir, '.virtualenvs'); +} + +function getWorkOnHome(): Promise { + // The WORKON_HOME variable contains the path to the root directory of all virtualenvwrapper environments. + // If the interpreter path belongs to one of them then it is a virtualenvwrapper type of environment. + const workOnHome = getEnvironmentVariable('WORKON_HOME'); + if (workOnHome) { + return Promise.resolve(workOnHome); + } + return getDefaultVirtualenvwrapperDir(); +} + +/** + * Checks if the given interpreter belongs to a virtualenvWrapper based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean}: Returns true if the interpreter belongs to a virtualenvWrapper environment. + */ +export async function isVirtualenvwrapperEnvironment(interpreterPath: string): Promise { + const workOnHomeDir = await getWorkOnHome(); + + // For environment to be a virtualenvwrapper based it has to follow these two rules: + // 1. It should be in a sub-directory under the WORKON_HOME + // 2. It should be a valid virtualenv environment + return ( + (await pathExists(workOnHomeDir)) && + isParentPath(interpreterPath, workOnHomeDir) && + isVirtualenvEnvironment(interpreterPath) + ); +} + +/** + * Extracts version information from pyvenv.cfg near a given interpreter. + * @param interpreterPath Absolute path to the interpreter + * + * Remarks: This function looks for pyvenv.cfg usually in the same or parent directory. + * Reads the pyvenv.cfg and finds the line that looks like 'version = 3.9.0`. Gets the + * version string from that lines and parses it. + */ +export async function getPythonVersionFromPyvenvCfg(interpreterPath: string): Promise { + const configPaths = getPyvenvConfigPathsFrom(interpreterPath); + let version = UNKNOWN_PYTHON_VERSION; + + // We want to check each of those locations in the order. There is no need to look at + // all of them in parallel. + for (const configPath of configPaths) { + if (await pathExists(configPath)) { + try { + const lines = splitLines(await readFile(configPath)); + + const pythonVersions = lines + .map((line) => { + const parts = line.split('='); + if (parts.length === 2) { + const name = parts[0].toLowerCase().trim(); + const value = parts[1].trim(); + if (name === 'version') { + try { + return parseVersion(value); + } catch (ex) { + return undefined; + } + } else if (name === 'version_info') { + try { + return parseVersionInfo(value); + } catch (ex) { + return undefined; + } + } + } + return undefined; + }) + .filter((v) => v !== undefined) + .map((v) => v!); + + if (pythonVersions.length > 0) { + for (const v of pythonVersions) { + if (comparePythonVersionSpecificity(v, version) > 0) { + version = v; + } + } + } + } catch (ex) { + // There is only ome pyvenv.cfg. If we found it but failed to parse it + // then just return here. No need to look for versions any further. + return UNKNOWN_PYTHON_VERSION; + } + } + } + + return version; +} + +/** + * Convert the given string into the corresponding Python version object. + * Example: + * 3.9.0.final.0 + * 3.9.0.alpha.1 + * 3.9.0.beta.2 + * 3.9.0.candidate.1 + * + * Does not parse: + * 3.9.0 + * 3.9.0a1 + * 3.9.0b2 + * 3.9.0rc1 + */ +function parseVersionInfo(versionInfoStr: string): PythonVersion { + let version: PythonVersion; + let after: string; + try { + [version, after] = parseBasicVersion(versionInfoStr); + } catch { + // XXX Use getEmptyVersion(). + return UNKNOWN_PYTHON_VERSION; + } + if (version.micro !== -1 && after.startsWith('.')) { + [version.release] = parseRelease(after); + } + return version; +} diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts new file mode 100644 index 000000000000..b0922f8bab06 --- /dev/null +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as fsapi from '../../common/platform/fs-paths'; +import { IWorkspaceService } from '../../common/application/types'; +import { ExecutionResult, IProcessServiceFactory, ShellOptions, SpawnOptions } from '../../common/process/types'; +import { IDisposable, IConfigurationService, IExperimentService } from '../../common/types'; +import { chain, iterable } from '../../common/utils/async'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { IServiceContainer } from '../../ioc/types'; +import { traceError, traceVerbose } from '../../logging'; + +let internalServiceContainer: IServiceContainer; +export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { + internalServiceContainer = serviceContainer; +} + +// processes + +export async function shellExecute(command: string, options: ShellOptions = {}): Promise> { + const useWorker = false; + const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; + return service.shellExec(command, options); +} + +export async function exec( + file: string, + args: string[], + options: SpawnOptions = {}, + useWorker = false, +): Promise> { + const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; + return service.exec(file, args, options); +} + +export function inExperiment(experimentName: string): boolean { + const service = internalServiceContainer.get(IExperimentService); + return service.inExperimentSync(experimentName); +} + +// Workspace + +export function isVirtualWorkspace(): boolean { + const service = internalServiceContainer.get(IWorkspaceService); + return service.isVirtualWorkspace; +} + +// filesystem + +export function pathExists(absPath: string): Promise { + return fsapi.pathExists(absPath); +} + +export function pathExistsSync(absPath: string): boolean { + return fsapi.pathExistsSync(absPath); +} + +export function readFile(filePath: string): Promise { + return fsapi.readFile(filePath, 'utf-8'); +} + +export function readFileSync(filePath: string): string { + return fsapi.readFileSync(filePath, 'utf-8'); +} + +/** + * Returns true if given file path exists within the given parent directory, false otherwise. + * @param filePath File path to check for + * @param parentPath The potential parent path to check for + */ +export function isParentPath(filePath: string, parentPath: string): boolean { + if (!parentPath.endsWith(path.sep)) { + parentPath += path.sep; + } + if (!filePath.endsWith(path.sep)) { + filePath += path.sep; + } + return normCasePath(filePath).startsWith(normCasePath(parentPath)); +} + +export async function isDirectory(filename: string): Promise { + const stat = await fsapi.lstat(filename); + return stat.isDirectory(); +} + +export function normalizePath(filename: string): string { + return path.normalize(filename); +} + +export function resolvePath(filename: string): string { + return path.resolve(filename); +} + +export function normCasePath(filePath: string): string { + return getOSType() === OSType.Windows ? path.normalize(filePath).toUpperCase() : path.normalize(filePath); +} + +export function arePathsSame(path1: string, path2: string): boolean { + return normCasePath(path1) === normCasePath(path2); +} + +export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats, count?: number): Promise { + stats = stats ?? (await fsapi.lstat(absPath)); + if (stats.isSymbolicLink()) { + if (count && count > 5) { + traceError(`Detected a potential symbolic link loop at ${absPath}, terminating resolution.`); + return absPath; + } + const link = await fsapi.readlink(absPath); + // Result from readlink is not guaranteed to be an absolute path. For eg. on Mac it resolves + // /usr/local/bin/python3.9 -> ../../../Library/Frameworks/Python.framework/Versions/3.9/bin/python3.9 + // + // The resultant path is reported relative to the symlink directory we resolve. Convert that to absolute path. + const absLinkPath = path.isAbsolute(link) ? link : path.resolve(path.dirname(absPath), link); + count = count ? count + 1 : 1; + return resolveSymbolicLink(absLinkPath, undefined, count); + } + return absPath; +} + +export async function getFileInfo(filePath: string): Promise<{ ctime: number; mtime: number }> { + try { + const data = await fsapi.lstat(filePath); + return { + ctime: data.ctime.valueOf(), + mtime: data.mtime.valueOf(), + }; + } catch (ex) { + // This can fail on some cases, such as, `reparse points` on windows. So, return the + // time as -1. Which we treat as not set in the extension. + traceVerbose(`Failed to get file info for ${filePath}`, ex); + return { ctime: -1, mtime: -1 }; + } +} + +export async function isFile(filePath: string): Promise { + const stats = await fsapi.lstat(filePath); + if (stats.isSymbolicLink()) { + const resolvedPath = await resolveSymbolicLink(filePath, stats); + const resolvedStats = await fsapi.lstat(resolvedPath); + return resolvedStats.isFile(); + } + return stats.isFile(); +} + +/** + * Returns full path to sub directories of a given directory. + * @param {string} root : path to get sub-directories from. + * @param options : If called with `resolveSymlinks: true`, then symlinks found in + * the directory are resolved and if they resolve to directories + * then resolved values are returned. + */ +export async function* getSubDirs( + root: string, + options?: { resolveSymlinks?: boolean }, +): AsyncIterableIterator { + const dirContents = await fsapi.readdir(root, { withFileTypes: true }); + const generators = dirContents.map((item) => { + async function* generator() { + const fullPath = path.join(root, item.name); + if (item.isDirectory()) { + yield fullPath; + } else if (options?.resolveSymlinks && item.isSymbolicLink()) { + // The current FS item is a symlink. It can potentially be a file + // or a directory. Resolve it first and then check if it is a directory. + const resolvedPath = await resolveSymbolicLink(fullPath); + const resolvedPathStat = await fsapi.lstat(resolvedPath); + if (resolvedPathStat.isDirectory()) { + yield resolvedPath; + } + } + } + + return generator(); + }); + + yield* iterable(chain(generators)); +} + +/** + * Returns the value for setting `python.`. + * @param name The name of the setting. + */ +export function getPythonSetting(name: string, root?: string): T | undefined { + const resource = root ? vscode.Uri.file(root) : undefined; + const settings = internalServiceContainer.get(IConfigurationService).getSettings(resource); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (settings as any)[name]; +} + +/** + * Registers the listener to be called when a particular setting changes. + * @param name The name of the setting. + * @param callback The listener function to be called when the setting changes. + */ +export function onDidChangePythonSetting(name: string, callback: () => void, root?: string): IDisposable { + return vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { + const scope = root ? vscode.Uri.file(root) : undefined; + if (event.affectsConfiguration(`python.${name}`, scope)) { + callback(); + } + }); +} diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts new file mode 100644 index 000000000000..8149706a5707 --- /dev/null +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as path from 'path'; +import { uniq } from 'lodash'; +import * as fsapi from '../../common/platform/fs-paths'; +import { getSearchPathEntries } from '../../common/utils/exec'; +import { resolveSymbolicLink } from './externalDependencies'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../logging'; + +/** + * Determine if the given filename looks like the simplest Python executable. + */ +export function matchBasicPythonBinFilename(filename: string): boolean { + return path.basename(filename) === 'python'; +} + +/** + * Checks if a given path matches pattern for standard non-windows python binary. + * @param {string} interpreterPath : Path to python interpreter. + * @returns {boolean} : Returns true if the path matches pattern for non-windows python binary. + */ +export function matchPythonBinFilename(filename: string): boolean { + /** + * This Reg-ex matches following file names: + * python + * python3 + * python38 + * python3.8 + */ + const posixPythonBinPattern = /^python(\d+(\.\d+)?)?$/; + + return posixPythonBinPattern.test(path.basename(filename)); +} + +export async function commonPosixBinPaths(): Promise { + const searchPaths = getSearchPathEntries(); + + const paths: string[] = Array.from( + new Set( + [ + '/bin', + '/etc', + '/lib', + '/lib/x86_64-linux-gnu', + '/lib64', + '/sbin', + '/snap/bin', + '/usr/bin', + '/usr/games', + '/usr/include', + '/usr/lib', + '/usr/lib/x86_64-linux-gnu', + '/usr/lib64', + '/usr/libexec', + '/usr/local', + '/usr/local/bin', + '/usr/local/etc', + '/usr/local/games', + '/usr/local/lib', + '/usr/local/sbin', + '/usr/sbin', + '/usr/share', + '~/.local/bin', + ].concat(searchPaths), + ), + ); + + const exists = await Promise.all(paths.map((p) => fsapi.pathExists(p))); + return paths.filter((_, index) => exists[index]); +} + +/** + * Finds python interpreter binaries or symlinks in a given directory. + * @param searchDir : Directory to search in + * @returns : Paths to python binaries found in the search directory. + */ +async function findPythonBinariesInDir(searchDir: string) { + return (await fs.promises.readdir(searchDir, { withFileTypes: true })) + .filter((dirent: fs.Dirent) => !dirent.isDirectory()) + .map((dirent: fs.Dirent) => path.join(searchDir, dirent.name)) + .filter(matchPythonBinFilename); +} + +/** + * Pick the shortest versions of the paths. The paths could be + * the binary itself or its symlink, whichever path is shorter. + * + * E.g: + * /usr/bin/python -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + * /usr/bin/python3 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + * /usr/bin/python3.7 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + * + * Of the 4 possible paths to same binary (3 symlinks and 1 binary path), + * the code below will pick '/usr/bin/python'. + */ +function pickShortestPath(pythonPaths: string[]) { + let shortestLen = pythonPaths[0].length; + let shortestPath = pythonPaths[0]; + for (const p of pythonPaths) { + if (p.length <= shortestLen) { + shortestLen = p.length; + shortestPath = p; + } + } + return shortestPath; +} + +/** + * Finds python binaries in given directories. This function additionally reduces the + * found binaries to unique set be resolving symlinks, and returns the shortest paths + * to the said unique binaries. + * @param searchDirs : Directories to search for python binaries + * @returns : Unique paths to python interpreters found in the search dirs. + */ +export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise { + const binToLinkMap = new Map(); + for (const searchDir of searchDirs) { + const paths = await findPythonBinariesInDir(searchDir).catch((ex) => { + traceWarn('Looking for python binaries within', searchDir, 'failed with', ex); + return []; + }); + + for (const filepath of paths) { + // Ensure that we have a collection of unique global binaries by + // resolving all symlinks to the target binaries. + try { + traceVerbose(`Attempting to resolve symbolic link: ${filepath}`); + const resolvedBin = await resolveSymbolicLink(filepath); + if (binToLinkMap.has(resolvedBin)) { + binToLinkMap.get(resolvedBin)?.push(filepath); + } else { + binToLinkMap.set(resolvedBin, [filepath]); + } + traceInfo(`Found: ${filepath} --> ${resolvedBin}`); + } catch (ex) { + traceError('Failed to resolve symbolic link: ', ex); + } + } + } + + // Pick the shortest versions of the paths. The paths could be + // the binary itself or its symlink, whichever path is shorter. + // + // E.g: + // /usr/bin/python -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + // /usr/bin/python3 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + // /usr/bin/python3.7 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + // + // Of the 4 possible paths to same binary (3 symlinks and 1 binary path), + // the code below will pick '/usr/bin/python'. + const keys = Array.from(binToLinkMap.keys()); + const pythonPaths = keys.map((key) => pickShortestPath([key, ...(binToLinkMap.get(key) ?? [])])); + return uniq(pythonPaths); +} diff --git a/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts b/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts new file mode 100644 index 000000000000..efc7d56409c8 --- /dev/null +++ b/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as minimatch from 'minimatch'; +import * as path from 'path'; +import { FileChangeType, watchLocationForPattern } from '../../common/platform/fileSystemWatcher'; +import { IDisposable } from '../../common/types'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { traceVerbose } from '../../logging'; + +const [executable, binName] = getOSType() === OSType.Windows ? ['python.exe', 'Scripts'] : ['python', 'bin']; + +/** + * Start watching the given directory for changes to files matching the glob. + * + * @param baseDir - the root to which the glob is applied while watching + * @param callback - called when the event happens + * @param executableGlob - matches the executable under the directory + */ +export function watchLocationForPythonBinaries( + baseDir: string, + callback: (type: FileChangeType, absPath: string) => void, + executableGlob: string = executable, +): IDisposable { + const resolvedGlob = path.posix.normalize(executableGlob); + const [baseGlob] = resolvedGlob.split('/').slice(-1); + function callbackClosure(type: FileChangeType, e: string) { + traceVerbose('Received event', type, JSON.stringify(e), 'for baseglob', baseGlob); + const isMatch = minimatch.default(path.basename(e), baseGlob, { nocase: getOSType() === OSType.Windows }); + if (!isMatch) { + // When deleting the file for some reason path to all directories leading up to python are reported + // Skip those events + return; + } + callback(type, e); + } + return watchLocationForPattern(baseDir, resolvedGlob, callbackClosure); +} + +// eslint-disable-next-line no-shadow +export enum PythonEnvStructure { + Standard = 'standard', + Flat = 'flat', +} + +/** + * Generate the globs to use when watching a directory for Python executables. + */ +export function resolvePythonExeGlobs( + basenameGlob = executable, + // Be default we always expect a "standard" structure. + structure = PythonEnvStructure.Standard, +): string[] { + if (path.posix.normalize(basenameGlob).includes('/')) { + throw Error(`invalid basename glob "${basenameGlob}"`); + } + const globs: string[] = []; + if (structure === PythonEnvStructure.Standard) { + globs.push( + // Check the directory. + basenameGlob, + // Check in all subdirectories. + `*/${basenameGlob}`, + // Check in the "bin" directory of all subdirectories. + `*/${binName}/${basenameGlob}`, + ); + } else if (structure === PythonEnvStructure.Flat) { + // Check only the directory. + globs.push(basenameGlob); + } + return globs; +} diff --git a/src/client/pythonEnvironments/common/registryKeys.worker.ts b/src/client/pythonEnvironments/common/registryKeys.worker.ts new file mode 100644 index 000000000000..05996d057f11 --- /dev/null +++ b/src/client/pythonEnvironments/common/registryKeys.worker.ts @@ -0,0 +1,24 @@ +import { Registry } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryKey } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryKeys(keys: IRegistryKey[]): IRegistryKey[] { + // Use the map function to create a new array with copies of the specified properties. + return keys.map((key) => ({ + hive: key.hive, + arch: key.arch, + key: key.key, + })); +} + +regKey.keys((err: Error, res: Registry[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryKeys(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/src/client/pythonEnvironments/common/registryValues.worker.ts b/src/client/pythonEnvironments/common/registryValues.worker.ts new file mode 100644 index 000000000000..eaef7cbd58a7 --- /dev/null +++ b/src/client/pythonEnvironments/common/registryValues.worker.ts @@ -0,0 +1,27 @@ +import { RegistryItem } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryValue } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryValues(values: IRegistryValue[]): IRegistryValue[] { + // Use the map function to create a new array with copies of the specified properties. + return values.map((value) => ({ + hive: value.hive, + arch: value.arch, + key: value.key, + name: value.name, + type: value.type, + value: value.value, + })); +} + +regKey.values((err: Error, res: RegistryItem[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryValues(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/src/client/pythonEnvironments/common/windowsRegistry.ts b/src/client/pythonEnvironments/common/windowsRegistry.ts new file mode 100644 index 000000000000..801ef0c907b1 --- /dev/null +++ b/src/client/pythonEnvironments/common/windowsRegistry.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { HKCU, HKLM, Options, REG_SZ, Registry, RegistryItem } from 'winreg'; +import * as path from 'path'; +import { createDeferred } from '../../common/utils/async'; +import { executeWorkerFile } from '../../common/process/worker/main'; + +export { HKCU, HKLM, REG_SZ, Options }; + +export interface IRegistryKey { + hive: string; + arch: string; + key: string; + parentKey?: IRegistryKey; +} + +export interface IRegistryValue { + hive: string; + arch: string; + key: string; + name: string; + type: string; + value: string; +} + +export async function readRegistryValues(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.values((err: Error, res: RegistryItem[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryValues.worker.js'), options); +} + +export async function readRegistryKeys(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.keys((err: Error, res: Registry[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryKeys.worker.js'), options); +} diff --git a/src/client/pythonEnvironments/common/windowsUtils.ts b/src/client/pythonEnvironments/common/windowsUtils.ts new file mode 100644 index 000000000000..fe15f71522a5 --- /dev/null +++ b/src/client/pythonEnvironments/common/windowsUtils.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { uniqBy } from 'lodash'; +import * as path from 'path'; +import { isTestExecution } from '../../common/constants'; +import { traceError, traceVerbose } from '../../logging'; +import { + HKCU, + HKLM, + IRegistryKey, + IRegistryValue, + readRegistryKeys, + readRegistryValues, + REG_SZ, +} from './windowsRegistry'; + +/* eslint-disable global-require */ + +/** + * Determine if the given filename looks like the simplest Python executable. + */ +export function matchBasicPythonBinFilename(filename: string): boolean { + return path.basename(filename).toLowerCase() === 'python.exe'; +} + +/** + * 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 matchPythonBinFilename(filename: 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(filename)); +} + +export interface IRegistryInterpreterData { + interpreterPath: string; + versionStr?: string; + sysVersionStr?: string; + bitnessStr?: string; + companyDisplayName?: string; + distroOrgName?: string; +} + +async function getInterpreterDataFromKey( + { arch, hive, key }: IRegistryKey, + distroOrgName: string, + useWorkerThreads: boolean, +): Promise { + const result: IRegistryInterpreterData = { + interpreterPath: '', + distroOrgName, + }; + + const values: IRegistryValue[] = await readRegistryValues({ arch, hive, key }, useWorkerThreads); + for (const value of values) { + switch (value.name) { + case 'SysArchitecture': + result.bitnessStr = value.value; + break; + case 'SysVersion': + result.sysVersionStr = value.value; + break; + case 'Version': + result.versionStr = value.value; + break; + case 'DisplayName': + result.companyDisplayName = value.value; + break; + default: + break; + } + } + + const subKeys: IRegistryKey[] = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); + const subKey = subKeys.map((s) => s.key).find((s) => s.endsWith('InstallPath')); + if (subKey) { + const subKeyValues: IRegistryValue[] = await readRegistryValues({ arch, hive, key: subKey }, useWorkerThreads); + const value = subKeyValues.find((v) => v.name === 'ExecutablePath'); + if (value) { + result.interpreterPath = value.value; + if (value.type !== REG_SZ) { + traceVerbose(`Registry interpreter path type [${value.type}]: ${value.value}`); + } + } + } + + if (result.interpreterPath.length > 0) { + return result; + } + return undefined; +} + +export async function getInterpreterDataFromRegistry( + arch: string, + hive: string, + key: string, + useWorkerThreads: boolean, +): Promise { + const subKeys = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); + const distroOrgName = key.substr(key.lastIndexOf('\\') + 1); + const allData = await Promise.all( + subKeys.map((subKey) => getInterpreterDataFromKey(subKey, distroOrgName, useWorkerThreads)), + ); + return (allData.filter((data) => data !== undefined) || []) as IRegistryInterpreterData[]; +} + +let registryInterpretersCache: IRegistryInterpreterData[] | undefined; + +/** + * Returns windows registry interpreters from memory, returns undefined if memory is empty. + * getRegistryInterpreters() must be called prior to this to populate memory. + */ +export function getRegistryInterpretersSync(): IRegistryInterpreterData[] | undefined { + return !isTestExecution() ? registryInterpretersCache : undefined; +} + +let registryInterpretersPromise: Promise | undefined; + +export async function getRegistryInterpreters(): Promise { + if (!isTestExecution() && registryInterpretersPromise !== undefined) { + return registryInterpretersPromise; + } + registryInterpretersPromise = getRegistryInterpretersImpl(); + return registryInterpretersPromise; +} + +async function getRegistryInterpretersImpl(useWorkerThreads = false): Promise { + let registryData: IRegistryInterpreterData[] = []; + + for (const arch of ['x64', 'x86']) { + for (const hive of [HKLM, HKCU]) { + const root = '\\SOFTWARE\\Python'; + let keys: string[] = []; + try { + keys = (await readRegistryKeys({ arch, hive, key: root }, useWorkerThreads)).map((k) => k.key); + } catch (ex) { + traceError(`Failed to access Registry: ${arch}\\${hive}\\${root}`, ex); + } + + for (const key of keys) { + registryData = registryData.concat( + await getInterpreterDataFromRegistry(arch, hive, key, useWorkerThreads), + ); + } + } + } + registryInterpretersCache = uniqBy(registryData, (r: IRegistryInterpreterData) => r.interpreterPath); + return registryInterpretersCache; +} diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts new file mode 100644 index 000000000000..8b6ffe1af450 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { Commands } from '../../../common/constants'; +import { Common } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { isWindows } from '../../../common/utils/platform'; + +export async function showErrorMessageWithLogs(message: string): Promise { + const result = await showErrorMessage(message, Common.openOutputPanel, Common.selectPythonInterpreter); + if (result === Common.openOutputPanel) { + await executeCommand(Commands.ViewOutput); + } else if (result === Common.selectPythonInterpreter) { + await executeCommand(Commands.Set_Interpreter); + } +} + +export function getVenvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.venv'); +} + +export async function hasVenv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(path.join(getVenvPath(workspaceFolder), 'pyvenv.cfg')); +} + +export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { + if (isWindows()) { + return path.join(getVenvPath(workspaceFolder), 'Scripts', 'python.exe'); + } + return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); +} + +export function getPrefixCondaEnvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.conda'); +} + +export async function hasPrefixCondaEnv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getPrefixCondaEnvPath(workspaceFolder)); +} diff --git a/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts new file mode 100644 index 000000000000..eccbf64a7866 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import * as fsapi from '../../../common/platform/fs-paths'; +import { getPipRequirementsFiles } from '../provider/venvUtils'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { PythonExtension } from '../../../api/types'; +import { traceVerbose } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { getWorkspaceStateValue } from '../../../common/persistentState'; + +export const CREATE_ENV_TRIGGER_SETTING_PART = 'createEnvironment.trigger'; +export const CREATE_ENV_TRIGGER_SETTING = `python.${CREATE_ENV_TRIGGER_SETTING_PART}`; + +export async function fileContainsInlineDependencies(_uri: Uri): Promise { + // This is a placeholder for the real implementation of inline dependencies support + // For now we don't detect anything. Once PEP-722/PEP-723 are accepted we can implement + // this properly. + return false; +} + +export async function hasRequirementFiles(workspace: WorkspaceFolder): Promise { + const files = await getPipRequirementsFiles(workspace); + const found = (files?.length ?? 0) > 0; + if (found) { + traceVerbose(`Found requirement files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function hasKnownFiles(workspace: WorkspaceFolder): Promise { + const filePaths: string[] = [ + 'poetry.lock', + 'conda.yaml', + 'environment.yaml', + 'conda.yml', + 'environment.yml', + 'Pipfile', + 'Pipfile.lock', + ].map((fileName) => path.join(workspace.uri.fsPath, fileName)); + const result = await Promise.all(filePaths.map((f) => fsapi.pathExists(f))); + const found = result.some((r) => r); + if (found) { + traceVerbose(`Found known files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function isGlobalPythonSelected(workspace: WorkspaceFolder): Promise { + const extension = getExtension(PVSC_EXTENSION_ID); + if (!extension) { + return false; + } + const extensionApi: PythonExtension = extension.exports as PythonExtension; + const interpreter = extensionApi.environments.getActiveEnvironmentPath(workspace.uri); + const details = await extensionApi.environments.resolveEnvironment(interpreter); + const isGlobal = details?.environment === undefined; + if (isGlobal) { + traceVerbose(`Selected python for [${workspace.uri.fsPath}] is [global] type: ${interpreter.path}`); + } + return isGlobal; +} + +/** + * Checks the setting `python.createEnvironment.trigger` to see if we should perform the checks + * to prompt to create an environment. + * Returns True if we should prompt to create an environment. + */ +export function shouldPromptToCreateEnv(): boolean { + const config = getConfiguration('python'); + if (config) { + const value = config.get(CREATE_ENV_TRIGGER_SETTING_PART, 'off'); + return value !== 'off'; + } + + return getWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off') !== 'off'; +} + +/** + * Sets `python.createEnvironment.trigger` to 'off' in the user settings. + */ +export function disableCreateEnvironmentTrigger(): void { + const config = getConfiguration('python'); + if (config) { + config.update('createEnvironment.trigger', 'off', ConfigurationTarget.Global); + } +} + +let _alreadyCreateEnvCriteriaCheck = false; +/** + * Run-once wrapper function for the workspace check to prompt to create an environment. + * @returns : True if we should prompt to c environment. + */ +export function isCreateEnvWorkspaceCheckNotRun(): boolean { + if (_alreadyCreateEnvCriteriaCheck) { + return false; + } + _alreadyCreateEnvCriteriaCheck = true; + return true; +} diff --git a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts new file mode 100644 index 000000000000..2d8925cc05f6 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticSeverity, l10n, Range, TextDocument, Uri } from 'vscode'; +import { installedCheckScript } from '../../../common/process/internal/scripts'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { traceInfo, traceVerbose, traceError } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { IInterpreterService } from '../../../interpreter/contracts'; + +interface PackageDiagnostic { + package: string; + line: number; + character: number; + endLine: number; + endCharacter: number; + code: string; + severity: DiagnosticSeverity; +} + +export const INSTALL_CHECKER_SOURCE = 'Python-InstalledPackagesChecker'; + +function parseDiagnostics(data: string): Diagnostic[] { + let diagnostics: Diagnostic[] = []; + try { + const raw = JSON.parse(data) as PackageDiagnostic[]; + diagnostics = raw.map((item) => { + const d = new Diagnostic( + new Range(item.line, item.character, item.endLine, item.endCharacter), + l10n.t('Package `{0}` is not installed in the selected environment.', item.package), + item.severity, + ); + d.code = { value: item.code, target: Uri.parse(`https://pypi.org/p/${item.package}`) }; + d.source = INSTALL_CHECKER_SOURCE; + return d; + }); + } catch { + diagnostics = []; + } + return diagnostics; +} + +function getMissingPackageSeverity(doc: TextDocument): number { + const config = getConfiguration('python', doc.uri); + const severity: string = config.get('missingPackage.severity', 'Hint'); + if (severity === 'Error') { + return DiagnosticSeverity.Error; + } + if (severity === 'Warning') { + return DiagnosticSeverity.Warning; + } + if (severity === 'Information') { + return DiagnosticSeverity.Information; + } + return DiagnosticSeverity.Hint; +} + +export async function getInstalledPackagesDiagnostics( + interpreterService: IInterpreterService, + doc: TextDocument, +): Promise { + const interpreter = await interpreterService.getActiveInterpreter(doc.uri); + if (!interpreter) { + return []; + } + const scriptPath = installedCheckScript(); + try { + traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath); + const envCopy = { ...process.env, VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}` }; + const result = await plainExec(interpreter.path, [scriptPath, doc.uri.fsPath], { + env: envCopy, + }); + traceVerbose('Installed packages check result:\n', result.stdout); + if (result.stderr) { + traceError('Installed packages check error:\n', result.stderr); + } + return parseDiagnostics(result.stdout); + } catch (ex) { + traceError('Error while getting installed packages check result:\n', ex); + } + return []; +} diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts new file mode 100644 index 000000000000..3ebab1c67fb4 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { CancellationToken, QuickPickItem, WorkspaceFolder } from 'vscode'; +import * as fsapi from '../../../common/platform/fs-paths'; +import { MultiStepAction, showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; + +function hasVirtualEnv(workspace: WorkspaceFolder): Promise { + return Promise.race([ + fsapi.pathExists(path.join(workspace.uri.fsPath, '.venv')), + fsapi.pathExists(path.join(workspace.uri.fsPath, '.conda')), + ]); +} + +async function getWorkspacesForQuickPick(workspaces: readonly WorkspaceFolder[]): Promise { + const items: QuickPickItem[] = []; + for (const workspace of workspaces) { + items.push({ + label: workspace.name, + detail: workspace.uri.fsPath, + description: (await hasVirtualEnv(workspace)) ? CreateEnv.hasVirtualEnv : undefined, + }); + } + + return items; +} + +export interface PickWorkspaceFolderOptions { + allowMultiSelect?: boolean; + token?: CancellationToken; + preSelectedWorkspace?: WorkspaceFolder; +} + +export async function pickWorkspaceFolder( + options?: PickWorkspaceFolderOptions, + context?: MultiStepAction, +): Promise { + const workspaces = getWorkspaceFolders(); + + if (!workspaces || workspaces.length === 0) { + if (context === MultiStepAction.Back) { + // No workspaces and nothing to show, should just go to previous + throw MultiStepAction.Back; + } + const result = await showErrorMessage(CreateEnv.noWorkspace, Common.openFolder); + if (result === Common.openFolder) { + await executeCommand('vscode.openFolder'); + } + return undefined; + } + + if (options?.preSelectedWorkspace) { + if (context === MultiStepAction.Back) { + // In this case there is no Quick Pick shown, should just go to previous + throw MultiStepAction.Back; + } + + return options.preSelectedWorkspace; + } + + if (workspaces.length === 1) { + if (context === MultiStepAction.Back) { + // In this case there is no Quick Pick shown, should just go to previous + throw MultiStepAction.Back; + } + + return workspaces[0]; + } + + // This is multi-root scenario. + const selected = await showQuickPickWithBack( + await getWorkspacesForQuickPick(workspaces), + { + placeHolder: CreateEnv.pickWorkspacePlaceholder, + ignoreFocusOut: true, + canPickMany: options?.allowMultiSelect, + matchOnDescription: true, + matchOnDetail: true, + }, + options?.token, + ); + + if (selected) { + if (Array.isArray(selected)) { + const details = selected.map((s: QuickPickItem) => s.detail).filter((s) => s !== undefined); + return workspaces.filter((w) => details.includes(w.uri.fsPath)); + } + return workspaces.filter((w) => w.uri.fsPath === (selected as QuickPickItem).detail)[0]; + } + + return undefined; +} diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts new file mode 100644 index 000000000000..899f57728804 --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, Disposable, QuickInputButtons } from 'vscode'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry, IPathUtils } from '../../common/types'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; +import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; +import { condaCreationProvider } from './provider/condaCreationProvider'; +import { VenvCreationProvider, VenvCreationProviderId } from './provider/venvCreationProvider'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; +import { + CreateEnvironmentProvider, + CreateEnvironmentOptions, + CreateEnvironmentResult, + ProposedCreateEnvironmentAPI, + EnvironmentDidCreateEvent, +} from './proposed.createEnvApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { CreateEnvironmentOptionsInternal } from './types'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { PythonEnvironment } from '../../envExt/types'; + +class CreateEnvironmentProviders { + private _createEnvProviders: CreateEnvironmentProvider[] = []; + + constructor() { + this._createEnvProviders = []; + } + + public add(provider: CreateEnvironmentProvider) { + if (this._createEnvProviders.filter((p) => p.id === provider.id).length > 0) { + throw new Error(`Create Environment provider with id ${provider.id} already registered`); + } + this._createEnvProviders.push(provider); + } + + public remove(provider: CreateEnvironmentProvider) { + this._createEnvProviders = this._createEnvProviders.filter((p) => p !== provider); + } + + public getAll(): readonly CreateEnvironmentProvider[] { + return this._createEnvProviders; + } +} + +const _createEnvironmentProviders: CreateEnvironmentProviders = new CreateEnvironmentProviders(); + +export function registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable { + _createEnvironmentProviders.add(provider); + return new Disposable(() => { + _createEnvironmentProviders.remove(provider); + }); +} + +export const { onCreateEnvironmentStarted, onCreateEnvironmentExited, isCreatingEnvironment } = getCreationEvents(); + +export function registerCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + pythonPathUpdater: IPythonPathUpdaterServiceManager, + pathUtils: IPathUtils, +): void { + disposables.push( + registerCommand( + Commands.Create_Environment, + async ( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, + ): Promise => { + if (useEnvExtension()) { + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: undefined, + pythonVersion: undefined, + }); + const result = await executeCommand( + 'python-envs.createAny', + options, + ); + if (result) { + const managerId = result.envId.managerId; + if (managerId === 'ms-python.python:venv') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + } + if (managerId === 'ms-python.python:conda') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } + return { path: result.environmentPath.path }; + } + } catch (err) { + if (err === QuickInputButtons.Back) { + return { workspaceFolder: undefined, action: 'Back' }; + } + throw err; + } + } else { + const providers = _createEnvironmentProviders.getAll(); + return handleCreateEnvironmentCommand(providers, options); + } + return undefined; + }, + ), + registerCommand( + Commands.Create_Environment_Button, + async (): Promise => { + sendTelemetryEvent(EventName.ENVIRONMENT_BUTTON, undefined, undefined); + await executeCommand(Commands.Create_Environment); + }, + ), + registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)), + registerCreateEnvironmentProvider(condaCreationProvider()), + onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => { + if (e.path && e.options?.selectEnvironment) { + await pythonPathUpdater.updatePythonPath( + e.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + e.workspaceFolder?.uri, + ); + showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`); + } + }), + ); +} + +export function buildEnvironmentCreationApi(): ProposedCreateEnvironmentAPI { + return { + onWillCreateEnvironment: onCreateEnvironmentStarted, + onDidCreateEnvironment: onCreateEnvironmentExited, + createEnvironment: async ( + options?: CreateEnvironmentOptions | undefined, + ): Promise => { + const providers = _createEnvironmentProviders.getAll(); + try { + return await handleCreateEnvironmentCommand(providers, options); + } catch (err) { + return { path: undefined, workspaceFolder: undefined, action: undefined, error: err as Error }; + } + }, + registerCreateEnvironmentProvider: (provider: CreateEnvironmentProvider) => + registerCreateEnvironmentProvider(provider), + }; +} + +export async function createVirtualEnvironment(options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal) { + const provider = _createEnvironmentProviders.getAll().find((p) => p.id === VenvCreationProviderId); + if (!provider) { + return; + } + return handleCreateEnvironmentCommand([provider], { ...options, providerId: provider.id }); +} diff --git a/src/client/pythonEnvironments/creation/createEnvButtonContext.ts b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts new file mode 100644 index 000000000000..4ce7d07ad69d --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { getConfiguration, onDidChangeConfiguration } from '../../common/vscodeApis/workspaceApis'; + +async function setShowCreateEnvButtonContextKey(): Promise { + const config = getConfiguration('python'); + const showCreateEnvButton = config.get('createEnvironment.contentButton', 'show') === 'show'; + await executeCommand('setContext', 'showCreateEnvButton', showCreateEnvButton); +} + +export function registerCreateEnvironmentButtonFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidChangeConfiguration(async () => { + await setShowCreateEnvButtonContextKey(); + }), + ); + + setShowCreateEnvButtonContextKey(); +} diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts new file mode 100644 index 000000000000..c7c4e84f445c --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Event, EventEmitter, QuickInputButtons, QuickPickItem } from 'vscode'; +import { CreateEnv } from '../../common/utils/localize'; +import { + MultiStepAction, + MultiStepNode, + showQuickPick, + showQuickPickWithBack, +} from '../../common/vscodeApis/windowApis'; +import { traceError, traceVerbose } from '../../logging'; +import { + CreateEnvironmentOptions, + CreateEnvironmentResult, + CreateEnvironmentProvider, + EnvironmentWillCreateEvent, + EnvironmentDidCreateEvent, +} from './proposed.createEnvApis'; +import { CreateEnvironmentOptionsInternal } from './types'; + +const onCreateEnvironmentStartedEvent = new EventEmitter(); +const onCreateEnvironmentExitedEvent = new EventEmitter(); + +let startedEventCount = 0; + +function isBusyCreatingEnvironment(): boolean { + return startedEventCount > 0; +} + +function fireStartedEvent(options?: CreateEnvironmentOptions): void { + onCreateEnvironmentStartedEvent.fire({ options }); + startedEventCount += 1; +} + +function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void { + startedEventCount -= 1; + if (result) { + onCreateEnvironmentExitedEvent.fire({ options, ...result }); + } else if (error) { + onCreateEnvironmentExitedEvent.fire({ options, error }); + } +} + +export function getCreationEvents(): { + onCreateEnvironmentStarted: Event; + onCreateEnvironmentExited: Event; + isCreatingEnvironment: () => boolean; +} { + return { + onCreateEnvironmentStarted: onCreateEnvironmentStartedEvent.event, + onCreateEnvironmentExited: onCreateEnvironmentExitedEvent.event, + isCreatingEnvironment: isBusyCreatingEnvironment, + }; +} + +async function createEnvironment( + provider: CreateEnvironmentProvider, + options: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): Promise { + let result: CreateEnvironmentResult | undefined; + let err: Error | undefined; + try { + fireStartedEvent(options); + result = await provider.createEnvironment(options); + } catch (ex) { + if (ex === QuickInputButtons.Back) { + traceVerbose('Create Env: User clicked back button during environment creation'); + if (!options.showBackButton) { + return undefined; + } + } + err = ex as Error; + throw err; + } finally { + fireExitedEvent(result, options, err); + } + return result; +} + +interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { + id: string; +} + +async function showCreateEnvironmentQuickPick( + providers: readonly CreateEnvironmentProvider[], + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): Promise { + const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ + label: p.name, + description: p.description, + id: p.id, + })); + + if (options?.providerId) { + const provider = providers.find((p) => p.id === options.providerId); + if (provider) { + return provider; + } + } + + let selectedItem: CreateEnvironmentProviderQuickPickItem | CreateEnvironmentProviderQuickPickItem[] | undefined; + + if (options?.showBackButton) { + selectedItem = await showQuickPickWithBack(items, { + placeHolder: CreateEnv.providersQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }); + } else { + selectedItem = await showQuickPick(items, { + placeHolder: CreateEnv.providersQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }); + } + + if (selectedItem) { + const selected = Array.isArray(selectedItem) ? selectedItem[0] : selectedItem; + if (selected) { + const selections = providers.filter((p) => p.id === selected.id); + if (selections.length > 0) { + return selections[0]; + } + } + } + return undefined; +} + +function getOptionsWithDefaults( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): CreateEnvironmentOptions & CreateEnvironmentOptionsInternal { + return { + installPackages: true, + ignoreSourceControl: true, + showBackButton: false, + selectEnvironment: true, + ...options, + }; +} + +export async function handleCreateEnvironmentCommand( + providers: readonly CreateEnvironmentProvider[], + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): Promise { + const optionsWithDefaults = getOptionsWithDefaults(options); + let selectedProvider: CreateEnvironmentProvider | undefined; + const envTypeStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + if (providers.length > 0) { + try { + selectedProvider = await showCreateEnvironmentQuickPick(providers, optionsWithDefaults); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (!selectedProvider) { + return MultiStepAction.Cancel; + } + } else { + traceError('No Environment Creation providers were registered.'); + if (context === MultiStepAction.Back) { + // There are no providers to select, so just step back. + return MultiStepAction.Back; + } + } + return MultiStepAction.Continue; + }, + undefined, + ); + + let result: CreateEnvironmentResult | undefined; + const createStep = new MultiStepNode( + envTypeStep, + async (context?: MultiStepAction) => { + if (context === MultiStepAction.Back) { + // This step is to trigger creation, which can go into other extension. + return MultiStepAction.Back; + } + if (selectedProvider) { + try { + result = await createEnvironment(selectedProvider, optionsWithDefaults); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } + return MultiStepAction.Continue; + }, + undefined, + ); + envTypeStep.next = createStep; + + const action = await MultiStepNode.run(envTypeStep); + if (options?.showBackButton) { + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + result = { action, workspaceFolder: undefined, path: undefined, error: undefined }; + } + } + + if (result) { + return Object.freeze(result); + } + return undefined; +} diff --git a/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts new file mode 100644 index 000000000000..5119290a0c2d --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { + fileContainsInlineDependencies, + hasKnownFiles, + hasRequirementFiles, + isGlobalPythonSelected, + shouldPromptToCreateEnv, + isCreateEnvWorkspaceCheckNotRun, + disableCreateEnvironmentTrigger, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { hasPrefixCondaEnv, hasVenv } from './common/commonUtils'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { Common, CreateEnv } from '../../common/utils/localize'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { Commands } from '../../common/constants'; +import { Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export enum CreateEnvironmentCheckKind { + /** + * Checks if environment creation is needed based on file location and content. + */ + File = 'file', + + /** + * Checks if environment creation is needed based on workspace contents. + */ + Workspace = 'workspace', +} + +export interface CreateEnvironmentTriggerOptions { + force?: boolean; +} + +async function createEnvironmentCheckForWorkspace(uri: Uri): Promise { + const workspace = getWorkspaceFolder(uri); + if (!workspace) { + traceInfo(`CreateEnv Trigger - Workspace not found for ${uri.fsPath}`); + return; + } + + const missingRequirements = async (workspaceFolder: WorkspaceFolder) => + !(await hasRequirementFiles(workspaceFolder)); + + const isNonGlobalPythonSelected = async (workspaceFolder: WorkspaceFolder) => + !(await isGlobalPythonSelected(workspaceFolder)); + + // Skip showing the Create Environment prompt if one of the following is True: + // 1. The workspace already has a ".venv" or ".conda" env + // 2. The workspace does NOT have "requirements.txt" or "requirements/*.txt" files + // 3. The workspace has known files for other environment types like environment.yml, conda.yml, poetry.lock, etc. + // 4. The selected python is NOT classified as a global python interpreter + const skipPrompt: boolean = ( + await Promise.all([ + hasVenv(workspace), + hasPrefixCondaEnv(workspace), + missingRequirements(workspace), + hasKnownFiles(workspace), + isNonGlobalPythonSelected(workspace), + ]) + ).some((r) => r); + + if (skipPrompt) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-not-met' }); + traceInfo(`CreateEnv Trigger - Skipping for ${uri.fsPath}`); + return; + } + + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-met' }); + const selection = await showInformationMessage( + CreateEnv.Trigger.workspaceTriggerMessage, + CreateEnv.Trigger.createEnvironment, + Common.doNotShowAgain, + ); + + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + await executeCommand(Commands.Create_Environment); + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === Common.doNotShowAgain) { + disableCreateEnvironmentTrigger(); + } +} + +function runOnceWorkspaceCheck(uri: Uri, options: CreateEnvironmentTriggerOptions = {}): Promise { + if (isCreateEnvWorkspaceCheckNotRun() || options?.force) { + return createEnvironmentCheckForWorkspace(uri); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'already-ran' }); + traceVerbose('CreateEnv Trigger - skipping this because it was already run'); + return Promise.resolve(); +} + +async function createEnvironmentCheckForFile(uri: Uri, options?: CreateEnvironmentTriggerOptions): Promise { + if (await fileContainsInlineDependencies(uri)) { + // TODO: Handle create environment for each file here. + // pending acceptance of PEP-722/PEP-723 + + // For now we do the same thing as for workspace. + await runOnceWorkspaceCheck(uri, options); + } + + // If the file does not have any inline dependencies, then we do the same thing + // as for workspace. + await runOnceWorkspaceCheck(uri, options); +} + +export async function triggerCreateEnvironmentCheck( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): Promise { + if (!uri) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'no-uri' }); + traceVerbose('CreateEnv Trigger - Skipping No URI provided'); + return; + } + + if (shouldPromptToCreateEnv()) { + if (kind === CreateEnvironmentCheckKind.File) { + await createEnvironmentCheckForFile(uri, options); + } else { + await runOnceWorkspaceCheck(uri, options); + } + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'turned-off' }); + traceVerbose('CreateEnv Trigger - turned off in settings'); + } +} + +export function triggerCreateEnvironmentCheckNonBlocking( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): void { + // The Event loop for Node.js runs functions with setTimeout() with lower priority than setImmediate. + // This is done to intentionally avoid blocking anything that the user wants to do. + setTimeout(() => triggerCreateEnvironmentCheck(kind, uri, options).ignoreErrors(), 0); +} + +export function registerCreateEnvironmentTriggers(disposables: Disposable[]): void { + disposables.push( + registerCommand(Commands.Create_Environment_Check, (file: Resource) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'as-command' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file, { force: true }); + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts b/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts new file mode 100644 index 000000000000..76a55bea19a0 --- /dev/null +++ b/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts @@ -0,0 +1,85 @@ +import { Disposable, TerminalShellExecutionStartEvent } from 'vscode'; +import { + disableCreateEnvironmentTrigger, + isGlobalPythonSelected, + shouldPromptToCreateEnv, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; +import { Common, CreateEnv } from '../../common/utils/localize'; +import { traceError, traceInfo } from '../../logging'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { Commands, PVSC_EXTENSION_ID } from '../../common/constants'; +import { CreateEnvironmentResult } from './proposed.createEnvApis'; +import { onDidStartTerminalShellExecution, showWarningMessage } from '../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +function checkCommand(command: string): boolean { + const lower = command.toLowerCase(); + return ( + lower.startsWith('pip install') || + lower.startsWith('pip3 install') || + lower.startsWith('python -m pip install') || + lower.startsWith('python3 -m pip install') + ); +} + +export function registerTriggerForPipInTerminal(disposables: Disposable[]): void { + if (!shouldPromptToCreateEnv()) { + return; + } + + const folders = getWorkspaceFolders(); + if (!folders || folders.length === 0) { + return; + } + + const createEnvironmentTriggered: Map = new Map(); + folders.forEach((workspaceFolder) => { + createEnvironmentTriggered.set(workspaceFolder.uri.fsPath, false); + }); + + disposables.push( + onDidStartTerminalShellExecution(async (e: TerminalShellExecutionStartEvent) => { + const workspaceFolder = getWorkspaceFolder(e.shellIntegration.cwd); + if ( + workspaceFolder && + !createEnvironmentTriggered.get(workspaceFolder.uri.fsPath) && + (await isGlobalPythonSelected(workspaceFolder)) + ) { + if (e.execution.commandLine.isTrusted && checkCommand(e.execution.commandLine.value)) { + createEnvironmentTriggered.set(workspaceFolder.uri.fsPath, true); + sendTelemetryEvent(EventName.ENVIRONMENT_TERMINAL_GLOBAL_PIP); + const selection = await showWarningMessage( + CreateEnv.Trigger.globalPipInstallTriggerMessage, + CreateEnv.Trigger.createEnvironment, + Common.doNotShowAgain, + ); + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + const result: CreateEnvironmentResult = await executeCommand(Commands.Create_Environment, { + workspaceFolder, + providerId: `${PVSC_EXTENSION_ID}:venv`, + }); + if (result.path) { + traceInfo('CreateEnv Trigger - Environment created: ', result.path); + traceInfo( + `CreateEnv Trigger - Running: ${ + result.path + } -m ${e.execution.commandLine.value.trim()}`, + ); + e.shellIntegration.executeCommand( + `${result.path} -m ${e.execution.commandLine.value}`.trim(), + ); + } + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === Common.doNotShowAgain) { + disableCreateEnvironmentTrigger(); + } + } + } + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts new file mode 100644 index 000000000000..0b55e1ec5ce1 --- /dev/null +++ b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticCollection, TextDocument, Uri } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { createDiagnosticCollection, onDidChangeDiagnostics } from '../../common/vscodeApis/languageApis'; +import { getActiveTextEditor, onDidChangeActiveTextEditor } from '../../common/vscodeApis/windowApis'; +import { + getOpenTextDocuments, + onDidCloseTextDocument, + onDidOpenTextDocument, + onDidSaveTextDocument, +} from '../../common/vscodeApis/workspaceApis'; +import { traceVerbose } from '../../logging'; +import { getInstalledPackagesDiagnostics, INSTALL_CHECKER_SOURCE } from './common/installCheckUtils'; +import { IInterpreterService } from '../../interpreter/contracts'; + +export const DEPS_NOT_INSTALLED_KEY = 'pythonDepsNotInstalled'; + +async function setContextForActiveEditor(diagnosticCollection: DiagnosticCollection): Promise { + const doc = getActiveTextEditor()?.document; + if (doc && (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml'))) { + const diagnostics = diagnosticCollection.get(doc.uri); + if (diagnostics && diagnostics.length > 0) { + traceVerbose(`Setting context for python dependencies not installed: ${doc.uri.fsPath}`); + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, true); + return; + } + } + + // undefined here in the logs means no file was selected + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, false); +} + +export function registerInstalledPackagesDiagnosticsProvider( + disposables: IDisposableRegistry, + interpreterService: IInterpreterService, +): void { + const diagnosticCollection = createDiagnosticCollection(INSTALL_CHECKER_SOURCE); + const updateDiagnostics = (uri: Uri, diagnostics: Diagnostic[]) => { + if (diagnostics.length > 0) { + diagnosticCollection.set(uri, diagnostics); + } else if (diagnosticCollection.has(uri)) { + diagnosticCollection.delete(uri); + } + }; + + disposables.push(diagnosticCollection); + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidCloseTextDocument((e: TextDocument) => { + updateDiagnostics(e.uri, []); + }), + onDidChangeDiagnostics(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + onDidChangeActiveTextEditor(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + interpreterService.onDidChangeInterpreter(() => { + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); + }), + ); + + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); +} diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts new file mode 100644 index 000000000000..ea520fdd27e2 --- /dev/null +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Event, Disposable, WorkspaceFolder } from 'vscode'; +import { EnvironmentTools } from '../../api/types'; + +export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; +export type EnvironmentProviderId = string; + +/** + * Options used when creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Default `true`. If `true`, the environment creation handler is expected to install packages. + */ + installPackages?: boolean; + + /** + * Default `true`. If `true`, the environment creation provider is expected to add the environment to ignore list + * for the source control. + */ + ignoreSourceControl?: boolean; + + /** + * Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput. + */ + showBackButton?: boolean; + + /** + * Default `true`. If `true`, the environment after creation will be selected. + */ + selectEnvironment?: boolean; +} + +/** + * Params passed on `onWillCreateEnvironment` event handler. + */ +export interface EnvironmentWillCreateEvent { + /** + * Options used to create a Python environment. + */ + readonly options: CreateEnvironmentOptions | undefined; +} + +export type CreateEnvironmentResult = + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error: Error; + }; + +/** + * Params passed on `onDidCreateEnvironment` event handler. + */ +export type EnvironmentDidCreateEvent = CreateEnvironmentResult & { + /** + * Options used to create the Python environment. + */ + readonly options: CreateEnvironmentOptions | undefined; +}; + +/** + * Extensions that want to contribute their own environment creation can do that by registering an object + * that implements this interface. + */ +export interface CreateEnvironmentProvider { + /** + * This API is called when user selects this provider from a QuickPick to select the type of environment + * user wants. This API is expected to show a QuickPick or QuickInput to get the user input and return + * the path to the Python executable in the environment. + * + * @param {CreateEnvironmentOptions} [options] Options used to create a Python environment. + * + * @returns a promise that resolves to the path to the + * Python executable in the environment. Or any action taken by the user, such as back or cancel. + */ + createEnvironment(options?: CreateEnvironmentOptions): Promise; + + /** + * Unique ID for the creation provider, typically : + */ + id: EnvironmentProviderId; + + /** + * Display name for the creation provider. + */ + name: string; + + /** + * Description displayed to the user in the QuickPick to select environment provider. + */ + description: string; + + /** + * Tools used to manage this environment. e.g., ['conda']. In the most to least priority order + * for resolving and working with the environment. + */ + tools: EnvironmentTools[]; +} + +export interface ProposedCreateEnvironmentAPI { + /** + * This API can be used to detect when the environment creation starts for any registered + * provider (including internal providers). This will also receive any options passed in + * or defaults used to create environment. + */ + readonly onWillCreateEnvironment: Event; + + /** + * This API can be used to detect when the environment provider exits for any registered + * provider (including internal providers). This will also receive created environment path, + * any errors, or user actions taken from the provider. + */ + readonly onDidCreateEnvironment: Event; + + /** + * This API will show a QuickPick to select an environment provider from available list of + * providers. Based on the selection the `createEnvironment` will be called on the provider. + */ + createEnvironment(options?: CreateEnvironmentOptions): Promise; + + /** + * This API should be called to register an environment creation provider. It returns + * a (@link Disposable} which can be used to remove the registration. + */ + registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts new file mode 100644 index 000000000000..a7e4e9a21cd1 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; +import * as path from 'path'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; +import { traceError, traceInfo, traceLog } from '../../../logging'; +import { CreateEnvironmentProgress } from '../types'; +import { pickWorkspaceFolder } from '../common/workspaceSelection'; +import { execObservable } from '../../../common/process/rawProcessApis'; +import { createDeferred } from '../../../common/utils/async'; +import { getOSType, OSType } from '../../../common/utils/platform'; +import { createCondaScript } from '../../../common/process/internal/scripts'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { + ExistingCondaAction, + deleteEnvironment, + getCondaBaseEnv, + getPathEnvVariableForConda, + pickExistingCondaAction, + pickPythonVersion, +} from './condaUtils'; +import { getPrefixCondaEnvPath, showErrorMessageWithLogs } from '../common/commonUtils'; +import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { + CondaProgressAndTelemetry, + CONDA_ENV_CREATED_MARKER, + CONDA_ENV_EXISTING_MARKER, +} from './condaProgressAndTelemetry'; +import { splitLines } from '../../../common/stringUtils'; +import { + CreateEnvironmentOptions, + CreateEnvironmentResult, + CreateEnvironmentProvider, +} from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; + +function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { + let addGitIgnore = true; + let installPackages = true; + if (options) { + addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; + installPackages = options?.installPackages !== undefined ? options.installPackages : true; + } + + const command: string[] = [createCondaScript()]; + + if (addGitIgnore) { + command.push('--git-ignore'); + } + + if (installPackages) { + command.push('--install'); + } + + if (version) { + command.push('--python'); + command.push(version); + } + + return command; +} + +function getCondaEnvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(CONDA_ENV_CREATED_MARKER) || s.startsWith(CONDA_ENV_EXISTING_MARKER))[0]; + if (envPath.includes(CONDA_ENV_CREATED_MARKER)) { + return envPath.substring(CONDA_ENV_CREATED_MARKER.length); + } + return envPath.substring(CONDA_ENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + +async function createCondaEnv( + workspace: WorkspaceFolder, + command: string, + args: string[], + progress: CreateEnvironmentProgress, + token?: CancellationToken, +): Promise { + progress.report({ + message: CreateEnv.Conda.creating, + }); + + const deferred = createDeferred(); + const pathEnv = getPathEnvVariableForConda(command); + traceLog('Running Conda Env creation script: ', [command, ...args]); + const { proc, out, dispose } = execObservable(command, args, { + mergeStdOutErr: true, + token, + cwd: workspace.uri.fsPath, + env: { + PATH: pathEnv, + }, + }); + + const progressAndTelemetry = new CondaProgressAndTelemetry(progress); + let condaEnvPath: string | undefined; + out.subscribe( + (value) => { + const output = splitLines(value.out).join('\r\n'); + traceLog(output.trimEnd()); + if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) { + condaEnvPath = getCondaEnvFromOutput(output); + } + progressAndTelemetry.process(output); + }, + async (error) => { + traceError('Error while running conda env creation script: ', error); + deferred.reject(error); + }, + () => { + dispose(); + if (proc?.exitCode !== 0) { + traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || `Conda env creation failed with exitCode: ${proc?.exitCode}`, + ); + } else { + deferred.resolve(condaEnvPath); + } + }, + ); + return deferred.promise; +} + +function getExecutableCommand(condaBaseEnvPath: string): string { + if (getOSType() === OSType.Windows) { + // Both Miniconda3 and Anaconda3 have the following structure: + // Miniconda3 (or Anaconda3) + // |- python.exe <--- this is the python that we want. + return path.join(condaBaseEnvPath, 'python.exe'); + } + // On non-windows machines: + // miniconda (or miniforge or anaconda3) + // |- bin + // |- python <--- this is the python that we want. + return path.join(condaBaseEnvPath, 'bin', 'python'); +} + +async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + const conda = await getCondaBaseEnv(); + if (!conda) { + return undefined; + } + + let workspace: WorkspaceFolder | undefined; + const workspaceStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + try { + workspace = (await pickWorkspaceFolder(undefined, context)) as WorkspaceFolder | undefined; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + + if (workspace === undefined) { + traceError('Workspace was not selected or found for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected workspace ${workspace.uri.fsPath} for creating conda environment.`); + return MultiStepAction.Continue; + }, + undefined, + ); + + let existingCondaAction: ExistingCondaAction | undefined; + const existingEnvStep = new MultiStepNode( + workspaceStep, + async (context?: MultiStepAction) => { + if (workspace && context === MultiStepAction.Continue) { + try { + existingCondaAction = await pickExistingCondaAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + + let version: string | undefined; + const versionStep = new MultiStepNode( + workspaceStep, + async (context) => { + if ( + existingCondaAction === ExistingCondaAction.Recreate || + existingCondaAction === ExistingCondaAction.Create + ) { + try { + version = await pickPythonVersion(); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (version === undefined) { + traceError('Python version was not selected for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected Python version ${version} for creating conda environment.`); + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + } + + return MultiStepAction.Continue; + }, + undefined, + ); + existingEnvStep.next = versionStep; + + const action = await MultiStepNode.run(workspaceStep); + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + throw action; + } + + if (workspace) { + if (existingCondaAction === ExistingCondaAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, getExecutableCommand(conda))) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'conda', + }); + return { path: getPrefixCondaEnvPath(workspace), workspaceFolder: workspace }; + } + } + + const createEnvInternal = async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'conda', + pythonVersion: version, + }); + if (workspace) { + envPath = await createCondaEnv( + workspace, + getExecutableCommand(conda), + generateCommandArgs(version, options), + progress, + token, + ); + + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + + throw new Error('Failed to create conda environment. See Output > Python for more info.'); + } else { + throw new Error('A workspace is needed to create conda environment'); + } + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } + + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => createEnvInternal(progress, token), + ); +} + +export function condaCreationProvider(): CreateEnvironmentProvider { + return { + createEnvironment, + name: 'Conda', + + description: CreateEnv.Conda.providerDescription, + + id: `${PVSC_EXTENSION_ID}:conda`, + + tools: ['Conda'], + }; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts new file mode 100644 index 000000000000..e4f4784f15c8 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { WorkspaceFolder } from 'vscode'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceInfo } from '../../../logging'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv, showErrorMessageWithLogs } from '../common/commonUtils'; + +export async function deleteCondaEnvironment( + workspace: WorkspaceFolder, + interpreter: string, + pathEnvVar: string, +): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspace); + const command = interpreter; + const args = ['-m', 'conda', 'env', 'remove', '--prefix', condaEnvPath, '--yes']; + try { + traceInfo(`Deleting conda environment: ${condaEnvPath}`); + traceInfo(`Running command: ${command} ${args.join(' ')}`); + const result = await plainExec(command, args, { mergeStdOutErr: true }, { ...process.env, PATH: pathEnvVar }); + traceInfo(result.stdout); + if (await hasPrefixCondaEnv(workspace)) { + // If conda cannot delete files it will name the files as .conda_trash. + // These need to be deleted manually. + traceError(`Conda environment ${condaEnvPath} could not be deleted.`); + traceError(`Please delete the environment manually: ${condaEnvPath}`); + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + return false; + } + } catch (err) { + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + traceError(`Deleting conda environment ${condaEnvPath} Failed with error: `, err); + return false; + } + return true; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts new file mode 100644 index 000000000000..304e90aec84f --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const CONDA_ENV_CREATED_MARKER = 'CREATED_CONDA_ENV:'; +export const CONDA_ENV_EXISTING_MARKER = 'EXISTING_CONDA_ENV:'; +export const CONDA_INSTALLING_YML = 'CONDA_INSTALLING_YML:'; +export const CREATE_CONDA_FAILED_MARKER = 'CREATE_CONDA.ENV_FAILED_CREATION'; +export const CREATE_CONDA_INSTALLED_YML = 'CREATE_CONDA.INSTALLED_YML'; +export const CREATE_FAILED_INSTALL_YML = 'CREATE_CONDA.FAILED_INSTALL_YML'; + +export class CondaProgressAndTelemetry { + private condaCreatedReported = false; + + private condaFailedReported = false; + + private condaInstallingPackagesReported = false; + + private condaInstallingPackagesFailedReported = false; + + private condaInstalledPackagesReported = false; + + private lastError: string | undefined = undefined; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public process(output: string): void { + if (!this.condaCreatedReported && output.includes(CONDA_ENV_CREATED_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } else if (!this.condaCreatedReported && output.includes(CONDA_ENV_EXISTING_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'existing', + }); + } else if (!this.condaFailedReported && output.includes(CREATE_CONDA_FAILED_MARKER)) { + this.condaFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'conda', + reason: 'other', + }); + this.lastError = CREATE_CONDA_FAILED_MARKER; + } else if (!this.condaInstallingPackagesReported && output.includes(CONDA_INSTALLING_YML)) { + this.condaInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Conda.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } else if (!this.condaInstallingPackagesFailedReported && output.includes(CREATE_FAILED_INSTALL_YML)) { + this.condaInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + this.lastError = CREATE_FAILED_INSTALL_YML; + } else if (!this.condaInstalledPackagesReported && output.includes(CREATE_CONDA_INSTALLED_YML)) { + this.condaInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } + } + + public getLastError(): string | undefined { + return this.lastError; + } +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts new file mode 100644 index 000000000000..617a2996801e --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { CancellationToken, ProgressLocation, QuickPickItem, Uri, WorkspaceFolder } from 'vscode'; +import { Commands, Octicons } from '../../../common/constants'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; +import { + MultiStepAction, + showErrorMessage, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; +import { traceLog } from '../../../logging'; +import { Conda } from '../../common/environmentManagers/conda'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv } from '../common/commonUtils'; +import { OSType, getEnvironmentVariable, getOSType } from '../../../common/utils/platform'; +import { deleteCondaEnvironment } from './condaDeleteUtils'; + +const RECOMMENDED_CONDA_PYTHON = '3.11'; + +export async function getCondaBaseEnv(): Promise { + const conda = await Conda.getConda(); + + if (!conda) { + const response = await showErrorMessage(CreateEnv.Conda.condaMissing, Common.learnMore); + if (response === Common.learnMore) { + await executeCommand('vscode.open', Uri.parse('https://docs.anaconda.com/anaconda/install/')); + } + return undefined; + } + + const envs = (await conda.getEnvList()).filter((e) => e.name === 'base'); + if (envs.length === 1) { + return envs[0].prefix; + } + if (envs.length > 1) { + traceLog( + 'Multiple conda base envs detected: ', + envs.map((e) => e.prefix), + ); + return undefined; + } + + return undefined; +} + +export async function pickPythonVersion(token?: CancellationToken): Promise { + const items: QuickPickItem[] = ['3.11', '3.12', '3.10', '3.9', '3.8'].map((v) => ({ + label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', + description: v, + })); + const selection = await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.selectPythonQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }, + token, + ); + + if (selection) { + return (selection as QuickPickItem).description; + } + + return undefined; +} + +export function getPathEnvVariableForConda(condaBasePythonPath: string): string { + const pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; + if (getOSType() === OSType.Windows) { + // On windows `conda.bat` is used, which adds the following bin directories to PATH + // then launches `conda.exe` which is a stub to `python.exe -m conda`. Here, we are + // instead using the `python.exe` that ships with conda to run a python script that + // handles conda env creation and package installation. + // See conda issue: https://github.com/conda/conda/issues/11399 + const root = path.dirname(condaBasePythonPath); + const libPath1 = path.join(root, 'Library', 'bin'); + const libPath2 = path.join(root, 'Library', 'mingw-w64', 'bin'); + const libPath3 = path.join(root, 'Library', 'usr', 'bin'); + const libPath4 = path.join(root, 'bin'); + const libPath5 = path.join(root, 'Scripts'); + const libPath = [libPath1, libPath2, libPath3, libPath4, libPath5].join(path.delimiter); + return `${libPath}${path.delimiter}${pathEnv}`; + } + return pathEnv; +} + +export async function deleteEnvironment(workspaceFolder: WorkspaceFolder, interpreter: string): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Conda.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${condaEnvPath}`, + cancellable: false, + }, + async () => deleteCondaEnvironment(workspaceFolder, interpreter, getPathEnvVariableForConda(interpreter)), + ); +} + +export enum ExistingCondaAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingCondaAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasPrefixCondaEnv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Conda.recreate, description: CreateEnv.Conda.recreateDescription }, + { + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.existingCondaQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Conda.recreate) { + return ExistingCondaAction.Recreate; + } + + if (selection?.label === CreateEnv.Conda.useExisting) { + return ExistingCondaAction.UseExisting; + } + } else { + return ExistingCondaAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts new file mode 100644 index 000000000000..5c29a8d7128d --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable } from 'vscode'; + +const envCreationTracker: Disposable[] = []; + +export function hideEnvCreation(): Disposable { + const disposable = new Disposable(() => { + const index = envCreationTracker.indexOf(disposable); + if (index > -1) { + envCreationTracker.splice(index, 1); + } + }); + envCreationTracker.push(disposable); + return disposable; +} + +export function shouldDisplayEnvCreationProgress(): boolean { + return envCreationTracker.length === 0; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts new file mode 100644 index 000000000000..c5c82b85357f --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -0,0 +1,389 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as os from 'os'; +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; +import { createVenvScript } from '../../../common/process/internal/scripts'; +import { execObservable } from '../../../common/process/rawProcessApis'; +import { createDeferred } from '../../../common/utils/async'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { CreateEnvironmentOptionsInternal, CreateEnvironmentProgress } from '../types'; +import { pickWorkspaceFolder } from '../common/workspaceSelection'; +import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; +import { EnvironmentType, PythonEnvironment } from '../../info'; +import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; +import { getVenvExecutable, showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingVenvAction, + IPackageInstallSelection, + deleteEnvironment, + pickExistingVenvAction, + pickPackagesToInstall, +} from './venvUtils'; +import { InputFlowAction } from '../../../common/utils/multiStepInput'; +import { + CreateEnvironmentProvider, + CreateEnvironmentOptions, + CreateEnvironmentResult, +} from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; + +interface IVenvCommandArgs { + argv: string[]; + stdin: string | undefined; +} + +function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): IVenvCommandArgs { + const command: string[] = [createVenvScript()]; + let stdin: string | undefined; + + if (addGitIgnore) { + command.push('--git-ignore'); + } + + if (installInfo) { + if (installInfo.some((i) => i.installType === 'toml')) { + const source = installInfo.find((i) => i.installType === 'toml')?.source; + command.push('--toml', source?.fileToCommandArgumentForPythonExt() || 'pyproject.toml'); + } + const extras = installInfo.filter((i) => i.installType === 'toml').map((i) => i.installItem); + extras.forEach((r) => { + if (r) { + command.push('--extras', r); + } + }); + + const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem); + + if (requirements.length < 10) { + requirements.forEach((r) => { + if (r) { + command.push('--requirements', r); + } + }); + } else { + command.push('--stdin'); + // Too many requirements can cause the command line to be too long error. + stdin = JSON.stringify({ requirements }); + } + } + + return { argv: command, stdin }; +} + +function getVenvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(VENV_CREATED_MARKER) || s.startsWith(VENV_EXISTING_MARKER))[0]; + if (envPath.includes(VENV_CREATED_MARKER)) { + return envPath.substring(VENV_CREATED_MARKER.length); + } + return envPath.substring(VENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + +async function createVenv( + workspace: WorkspaceFolder, + command: string, + args: IVenvCommandArgs, + progress: CreateEnvironmentProgress, + token?: CancellationToken, +): Promise { + progress.report({ + message: CreateEnv.Venv.creating, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'venv', + pythonVersion: undefined, + }); + + const deferred = createDeferred(); + traceLog('Running Env creation script: ', [command, ...args.argv]); + if (args.stdin) { + traceLog('Requirements passed in via stdin: ', args.stdin); + } + const { proc, out, dispose } = execObservable(command, args.argv, { + mergeStdOutErr: true, + token, + cwd: workspace.uri.fsPath, + stdinStr: args.stdin, + }); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progress); + let venvPath: string | undefined; + out.subscribe( + (value) => { + const output = value.out.split(/\r?\n/g).join(os.EOL); + traceLog(output.trimEnd()); + if (output.includes(VENV_CREATED_MARKER) || output.includes(VENV_EXISTING_MARKER)) { + venvPath = getVenvFromOutput(output); + } + progressAndTelemetry.process(output); + }, + (error) => { + traceError('Error while running venv creation script: ', error); + deferred.reject(error); + }, + () => { + dispose(); + if (proc?.exitCode !== 0) { + traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || + `Failed to create virtual environment with exitCode: ${proc?.exitCode}`, + ); + } else { + deferred.resolve(venvPath); + } + }, + ); + return deferred.promise; +} + +export const VenvCreationProviderId = `${PVSC_EXTENSION_ID}:venv`; +export class VenvCreationProvider implements CreateEnvironmentProvider { + constructor(private readonly interpreterQuickPick: IInterpreterQuickPick) {} + + public async createEnvironment( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, + ): Promise { + let workspace = options?.workspaceFolder; + const bypassQuickPicks = options?.workspaceFolder && options.interpreter && options.providerId ? true : false; + const workspaceStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + try { + workspace = + workspace && bypassQuickPicks + ? workspace + : ((await pickWorkspaceFolder( + { preSelectedWorkspace: options?.workspaceFolder }, + context, + )) as WorkspaceFolder | undefined); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + + if (workspace === undefined) { + traceError('Workspace was not selected or found for creating virtual environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected workspace ${workspace.uri.fsPath} for creating virtual environment.`); + return MultiStepAction.Continue; + }, + undefined, + ); + + let existingVenvAction: ExistingVenvAction | undefined; + if (bypassQuickPicks) { + existingVenvAction = ExistingVenvAction.Create; + } + const existingEnvStep = new MultiStepNode( + workspaceStep, + async (context?: MultiStepAction) => { + if (workspace && context === MultiStepAction.Continue) { + try { + existingVenvAction = await pickExistingVenvAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + + let interpreter = options?.interpreter; + const interpreterStep = new MultiStepNode( + existingEnvStep, + async (context?: MultiStepAction) => { + if (workspace) { + if ( + existingVenvAction === ExistingVenvAction.Recreate || + existingVenvAction === ExistingVenvAction.Create + ) { + try { + interpreter = + interpreter && bypassQuickPicks + ? interpreter + : await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters + { + skipRecommended: true, + showBackButton: true, + placeholder: CreateEnv.Venv.selectPythonPlaceHolder, + title: null, + }, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + return MultiStepAction.Back; + } + interpreter = undefined; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + interpreter = getVenvExecutable(workspace); + } + } + + if (!interpreter) { + traceError('Virtual env creation requires an interpreter.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected interpreter ${interpreter} for creating virtual environment.`); + return MultiStepAction.Continue; + }, + undefined, + ); + existingEnvStep.next = interpreterStep; + + let addGitIgnore = true; + let installPackages = true; + if (options) { + addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; + installPackages = options?.installPackages !== undefined ? options.installPackages : true; + } + let installInfo: IPackageInstallSelection[] | undefined; + const packagesStep = new MultiStepNode( + interpreterStep, + async (context?: MultiStepAction) => { + if (workspace && installPackages) { + if (existingVenvAction !== ExistingVenvAction.UseExisting) { + try { + installInfo = await pickPackagesToInstall(workspace); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (!installInfo) { + traceVerbose('Virtual env creation exited during dependencies selection.'); + return MultiStepAction.Cancel; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + } + + return MultiStepAction.Continue; + }, + undefined, + ); + interpreterStep.next = packagesStep; + + const action = await MultiStepNode.run(workspaceStep); + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + throw action; + } + + if (workspace) { + if (existingVenvAction === ExistingVenvAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, interpreter)) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'venv', + }); + return { path: getVenvExecutable(workspace), workspaceFolder: workspace }; + } + } + + const args = generateCommandArgs(installInfo, addGitIgnore); + const createEnvInternal = async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + if (interpreter && workspace) { + envPath = await createVenv(workspace, interpreter, args, progress, token); + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + throw new Error('Failed to create virtual environment. See Output > Python for more info.'); + } + throw new Error('Failed to create virtual environment. Either interpreter or workspace is undefined.'); + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } + + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => createEnvInternal(progress, token), + ); + } + + name = 'Venv'; + + description: string = CreateEnv.Venv.providerDescription; + + id = VenvCreationProviderId; + + tools = ['Venv']; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts new file mode 100644 index 000000000000..9bd410c09f51 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { traceError, traceInfo } from '../../../logging'; +import { getVenvPath, showErrorMessageWithLogs } from '../common/commonUtils'; +import { CreateEnv } from '../../../common/utils/localize'; +import { sleep } from '../../../common/utils/async'; +import { switchSelectedPython } from './venvSwitchPython'; + +async function tryDeleteFile(file: string): Promise { + try { + if (!(await fs.pathExists(file))) { + return true; + } + await fs.unlink(file); + return true; + } catch (err) { + traceError(`Failed to delete file [${file}]:`, err); + return false; + } +} + +async function tryDeleteDir(dir: string): Promise { + try { + if (!(await fs.pathExists(dir))) { + return true; + } + await fs.rmdir(dir, { + recursive: true, + maxRetries: 10, + retryDelay: 200, + }); + return true; + } catch (err) { + traceError(`Failed to delete directory [${dir}]:`, err); + return false; + } +} + +export async function deleteEnvironmentNonWindows(workspaceFolder: WorkspaceFolder): Promise { + const venvPath = getVenvPath(workspaceFolder); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted venv dir: ${venvPath}`); + return true; + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} + +export async function deleteEnvironmentWindows( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + const venvPythonPath = path.join(venvPath, 'Scripts', 'python.exe'); + + if (await tryDeleteFile(venvPythonPath)) { + traceInfo(`Deleted python executable: ${venvPythonPath}`); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + + traceError(`Failed to delete ".venv" dir: ${venvPath}`); + traceError( + 'This happens if the virtual environment is still in use, or some binary in the venv is still running.', + ); + traceError(`Please delete the ".venv" manually: [${venvPath}]`); + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; + } + traceError(`Failed to delete python executable: ${venvPythonPath}`); + traceError('This happens if the virtual environment is still in use.'); + + if (interpreter) { + traceError('We will attempt to switch python temporarily to delete the ".venv"'); + + await switchSelectedPython(interpreter, workspaceFolder.uri, 'temporarily to delete the ".venv"'); + + traceInfo(`Attempting to delete ".venv" again: ${venvPath}`); + const ms = 500; + for (let i = 0; i < 5; i = i + 1) { + traceInfo(`Waiting for ${ms}ms to let processes exit, before a delete attempt.`); + await sleep(ms); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + traceError(`Failed to delete ".venv" dir [${venvPath}] (attempt ${i + 1}/5).`); + } + } else { + traceError(`Please delete the ".venv" dir manually: [${venvPath}]`); + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts new file mode 100644 index 000000000000..e092c40c3fe0 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const VENV_CREATED_MARKER = 'CREATED_VENV:'; +export const VENV_EXISTING_MARKER = 'EXISTING_VENV:'; +const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; +const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; +const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; +const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; +const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; +const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; +const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; +const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; +const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; +const UPGRADE_PIP_FAILED_MARKER = 'CREATE_VENV.UPGRADE_PIP_FAILED'; +const UPGRADING_PIP_MARKER = 'CREATE_VENV.UPGRADING_PIP'; +const UPGRADED_PIP_MARKER = 'CREATE_VENV.UPGRADED_PIP'; +const CREATING_MICROVENV_MARKER = 'CREATE_MICROVENV.CREATING_MICROVENV'; +const CREATE_MICROVENV_FAILED_MARKER = 'CREATE_VENV.MICROVENV_FAILED_CREATION'; +const CREATE_MICROVENV_FAILED_MARKER2 = 'CREATE_MICROVENV.MICROVENV_FAILED_CREATION'; +const MICROVENV_CREATED_MARKER = 'CREATE_MICROVENV.CREATED_MICROVENV'; +const INSTALLING_PIP_MARKER = 'CREATE_VENV.INSTALLING_PIP'; +const INSTALL_PIP_FAILED_MARKER = 'CREATE_VENV.INSTALL_PIP_FAILED'; +const DOWNLOADING_PIP_MARKER = 'CREATE_VENV.DOWNLOADING_PIP'; +const DOWNLOAD_PIP_FAILED_MARKER = 'CREATE_VENV.DOWNLOAD_PIP_FAILED'; +const DISTUTILS_NOT_INSTALLED_MARKER = 'CREATE_VENV.DISTUTILS_NOT_INSTALLED'; + +export class VenvProgressAndTelemetry { + private readonly processed = new Set(); + + private readonly reportActions = new Map string | undefined>([ + [ + VENV_CREATED_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.created }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + return undefined; + }, + ], + [ + VENV_EXISTING_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.existing }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLING_REQUIREMENTS, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLING_PYPROJECT, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + PIP_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noPip', + }); + return PIP_NOT_INSTALLED_MARKER; + }, + ], + [ + DISTUTILS_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noDistUtils', + }); + return VENV_NOT_INSTALLED_MARKER; + }, + ], + [ + VENV_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noVenv', + }); + return VENV_NOT_INSTALLED_MARKER; + }, + ], + [ + INSTALL_REQUIREMENTS_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return INSTALL_REQUIREMENTS_FAILED_MARKER; + }, + ], + [ + INSTALL_PYPROJECT_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return INSTALL_PYPROJECT_FAILED_MARKER; + }, + ], + [ + CREATE_VENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'other', + }); + return CREATE_VENV_FAILED_MARKER; + }, + ], + [ + VENV_ALREADY_EXISTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLED_REQUIREMENTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLED_PYPROJECT_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + UPGRADED_PIP_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + [ + UPGRADE_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return UPGRADE_PIP_FAILED_MARKER; + }, + ], + [ + DOWNLOADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.downloadingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return undefined; + }, + ], + [ + DOWNLOAD_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return DOWNLOAD_PIP_FAILED_MARKER; + }, + ], + [ + INSTALLING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return undefined; + }, + ], + [ + INSTALL_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return INSTALL_PIP_FAILED_MARKER; + }, + ], + [ + CREATING_MICROVENV_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.creatingMicrovenv }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'microvenv', + pythonVersion: undefined, + }); + return undefined; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER2, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER2; + }, + ], + [ + MICROVENV_CREATED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'microvenv', + reason: 'created', + }); + return undefined; + }, + ], + [ + UPGRADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.upgradingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + ]); + + private lastError: string | undefined = undefined; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public getLastError(): string | undefined { + return this.lastError; + } + + public process(output: string): void { + const keys: string[] = Array.from(this.reportActions.keys()); + + for (const key of keys) { + if (output.includes(key) && !this.processed.has(key)) { + const action = this.reportActions.get(key); + if (action) { + const err = action(this.progress); + if (err) { + this.lastError = err; + } + } + this.processed.add(key); + } + } + } +} diff --git a/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts new file mode 100644 index 000000000000..e2567dfd114b --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { createDeferred } from '../../../common/utils/async'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID, PythonExtension } from '../../../api/types'; +import { traceInfo } from '../../../logging'; + +export async function switchSelectedPython(interpreter: string, uri: Uri, purpose: string): Promise { + let dispose: Disposable | undefined; + try { + const deferred = createDeferred(); + const api: PythonExtension = getExtension(PVSC_EXTENSION_ID)?.exports as PythonExtension; + dispose = api.environments.onDidChangeActiveEnvironmentPath(async (e) => { + if (path.normalize(e.path) === path.normalize(interpreter)) { + traceInfo(`Switched to interpreter ${purpose}: ${interpreter}`); + deferred.resolve(); + } + }); + api.environments.updateActiveEnvironmentPath(interpreter, uri); + traceInfo(`Switching interpreter ${purpose}: ${interpreter}`); + await deferred.promise; + } finally { + dispose?.dispose(); + } +} diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts new file mode 100644 index 000000000000..1bfb2c96f224 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as tomljs from '@iarna/toml'; +import { flatten, isArray } from 'lodash'; +import * as path from 'path'; +import { + CancellationToken, + ProgressLocation, + QuickPickItem, + QuickPickItemButtonEvent, + RelativePattern, + ThemeIcon, + Uri, + WorkspaceFolder, +} from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { + MultiStepAction, + MultiStepNode, + showQuickPickWithBack, + showTextDocument, + withProgress, +} from '../../../common/vscodeApis/windowApis'; +import { findFiles } from '../../../common/vscodeApis/workspaceApis'; +import { traceError, traceVerbose } from '../../../logging'; +import { Commands } from '../../../common/constants'; +import { isWindows } from '../../../common/utils/platform'; +import { getVenvPath, hasVenv } from '../common/commonUtils'; +import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; + +export const OPEN_REQUIREMENTS_BUTTON = { + iconPath: new ThemeIcon('go-to-file'), + tooltip: CreateEnv.Venv.openRequirementsFile, +}; +const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; +export async function getPipRequirementsFiles( + workspaceFolder: WorkspaceFolder, + token?: CancellationToken, +): Promise { + const files = flatten( + await Promise.all([ + findFiles(new RelativePattern(workspaceFolder, '**/*requirement*.txt'), exclude, undefined, token), + findFiles(new RelativePattern(workspaceFolder, '**/requirements/*.txt'), exclude, undefined, token), + ]), + ).map((u) => u.fsPath); + return files; +} + +function tomlParse(content: string): tomljs.JsonMap { + try { + return tomljs.parse(content); + } catch (err) { + traceError('Failed to parse `pyproject.toml`:', err); + } + return {}; +} + +function tomlHasBuildSystem(toml: tomljs.JsonMap): boolean { + return toml['build-system'] !== undefined; +} + +function tomlHasProject(toml: tomljs.JsonMap): boolean { + return toml.project !== undefined; +} + +function getTomlOptionalDeps(toml: tomljs.JsonMap): string[] { + const extras: string[] = []; + if (toml.project && (toml.project as tomljs.JsonMap)['optional-dependencies']) { + const deps = (toml.project as tomljs.JsonMap)['optional-dependencies']; + for (const key of Object.keys(deps)) { + extras.push(key); + } + } + return extras; +} + +async function pickTomlExtras(extras: string[], token?: CancellationToken): Promise { + const items: QuickPickItem[] = extras.map((e) => ({ label: e })); + + const selection = await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + canPickMany: true, + ignoreFocusOut: true, + }, + token, + ); + + if (selection && isArray(selection)) { + return selection.map((s) => s.label); + } + + return undefined; +} + +async function pickRequirementsFiles( + files: string[], + root: string, + token?: CancellationToken, +): Promise { + const items: QuickPickItem[] = files + .map((p) => path.relative(root, p)) + .sort((a, b) => { + const al: number = a.split(/[\\\/]/).length; + const bl: number = b.split(/[\\\/]/).length; + if (al === bl) { + if (a.length === b.length) { + return a.localeCompare(b); + } + return a.length - b.length; + } + return al - bl; + }) + .map((e) => ({ + label: e, + buttons: [OPEN_REQUIREMENTS_BUTTON], + })); + + const selection = await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + token, + async (e: QuickPickItemButtonEvent) => { + if (e.item.label) { + await showTextDocument(Uri.file(path.join(root, e.item.label))); + } + }, + ); + + if (selection && isArray(selection)) { + return selection.map((s) => s.label); + } + + return undefined; +} + +export function isPipInstallableToml(tomlContent: string): boolean { + const toml = tomlParse(tomlContent); + return tomlHasBuildSystem(toml) && tomlHasProject(toml); +} + +export interface IPackageInstallSelection { + installType: 'toml' | 'requirements' | 'none'; + installItem?: string; + source?: string; +} + +export async function pickPackagesToInstall( + workspaceFolder: WorkspaceFolder, + token?: CancellationToken, +): Promise { + const tomlPath = path.join(workspaceFolder.uri.fsPath, 'pyproject.toml'); + const packages: IPackageInstallSelection[] = []; + + const tomlStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + traceVerbose(`Looking for toml pyproject.toml with optional dependencies at: ${tomlPath}`); + + let extras: string[] = []; + let hasBuildSystem = false; + let hasProject = false; + + if (await fs.pathExists(tomlPath)) { + const toml = tomlParse(await fs.readFile(tomlPath, 'utf-8')); + extras = getTomlOptionalDeps(toml); + hasBuildSystem = tomlHasBuildSystem(toml); + hasProject = tomlHasProject(toml); + + if (!hasProject) { + traceVerbose('Create env: Found toml without project. So we will not use editable install.'); + } + if (!hasBuildSystem) { + traceVerbose('Create env: Found toml without build system. So we will not use editable install.'); + } + if (extras.length === 0) { + traceVerbose('Create env: Found toml without optional dependencies.'); + } + } else if (context === MultiStepAction.Back) { + // This step is not really used so just go back + return MultiStepAction.Back; + } + + if (hasBuildSystem && hasProject) { + if (extras.length > 0) { + traceVerbose('Create Env: Found toml with optional dependencies.'); + + try { + const installList = await pickTomlExtras(extras, token); + if (installList) { + if (installList.length > 0) { + installList.forEach((i) => { + packages.push({ installType: 'toml', installItem: i, source: tomlPath }); + }); + } + packages.push({ installType: 'toml', source: tomlPath }); + } else { + return MultiStepAction.Cancel; + } + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + // This step is not really used so just go back + return MultiStepAction.Back; + } else { + // There are no extras to install and the context is to go to next step + packages.push({ installType: 'toml', source: tomlPath }); + } + } else if (context === MultiStepAction.Back) { + // This step is not really used because there is no build system in toml, so just go back + return MultiStepAction.Back; + } + + return MultiStepAction.Continue; + }, + undefined, + ); + + const requirementsStep = new MultiStepNode( + tomlStep, + async (context?: MultiStepAction) => { + traceVerbose('Looking for pip requirements.'); + const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token); + if (requirementFiles && requirementFiles.length > 0) { + traceVerbose('Found pip requirements.'); + try { + const result = await pickRequirementsFiles(requirementFiles, workspaceFolder.uri.fsPath, token); + const installList = result?.map((p) => path.join(workspaceFolder.uri.fsPath, p)); + if (installList) { + installList.forEach((i) => { + packages.push({ installType: 'requirements', installItem: i }); + }); + } else { + return MultiStepAction.Cancel; + } + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + // This step is not really used, because there were no requirement files, so just go back + return MultiStepAction.Back; + } + + return MultiStepAction.Continue; + }, + undefined, + ); + tomlStep.next = requirementsStep; + + const action = await MultiStepNode.run(tomlStep); + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + throw action; + } + + return packages; +} + +export async function deleteEnvironment( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Venv.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${venvPath}`, + cancellable: false, + }, + async () => { + if (isWindows()) { + return deleteEnvironmentWindows(workspaceFolder, interpreter); + } + return deleteEnvironmentNonWindows(workspaceFolder); + }, + ); +} + +export enum ExistingVenvAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingVenvAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasVenv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }, + { + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.existingVenvQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Venv.recreate) { + return ExistingVenvAction.Recreate; + } + + if (selection?.label === CreateEnv.Venv.useExisting) { + return ExistingVenvAction.UseExisting; + } + } else { + return ExistingVenvAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts new file mode 100644 index 000000000000..5925b7641f45 --- /dev/null +++ b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TextDocument } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { + onDidOpenTextDocument, + onDidSaveTextDocument, + getOpenTextDocuments, +} from '../../common/vscodeApis/workspaceApis'; +import { isPipInstallableToml } from './provider/venvUtils'; + +async function setPyProjectTomlContextKey(doc: TextDocument): Promise { + if (isPipInstallableToml(doc.getText())) { + await executeCommand('setContext', 'pipInstallableToml', true); + } else { + await executeCommand('setContext', 'pipInstallableToml', false); + } +} + +export function registerPyProjectTomlFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + ); + + const docs = getOpenTextDocuments().filter( + (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), + ); + if (docs.length > 0) { + executeCommand('setContext', 'pipInstallableToml', true); + } else { + executeCommand('setContext', 'pipInstallableToml', false); + } +} diff --git a/src/client/pythonEnvironments/creation/registrations.ts b/src/client/pythonEnvironments/creation/registrations.ts new file mode 100644 index 000000000000..25141cbec5ac --- /dev/null +++ b/src/client/pythonEnvironments/creation/registrations.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry, IPathUtils } from '../../common/types'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { registerCreateEnvironmentFeatures } from './createEnvApi'; +import { registerCreateEnvironmentButtonFeatures } from './createEnvButtonContext'; +import { registerTriggerForPipInTerminal } from './globalPipInTerminalTrigger'; +import { registerInstalledPackagesDiagnosticsProvider } from './installedPackagesDiagnostic'; +import { registerPyProjectTomlFeatures } from './pyProjectTomlContext'; + +export function registerAllCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + pythonPathUpdater: IPythonPathUpdaterServiceManager, + interpreterService: IInterpreterService, + pathUtils: IPathUtils, +): void { + registerCreateEnvironmentFeatures(disposables, interpreterQuickPick, pythonPathUpdater, pathUtils); + registerCreateEnvironmentButtonFeatures(disposables); + registerPyProjectTomlFeatures(disposables); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService); + registerTriggerForPipInTerminal(disposables); +} diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts new file mode 100644 index 000000000000..0e400c2d90f3 --- /dev/null +++ b/src/client/pythonEnvironments/creation/types.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Progress, WorkspaceFolder } from 'vscode'; + +export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} + +/** + * The interpreter path to use for the environment creation. If not provided, will prompt the user to select one. + * If the value of `interpreter` & `workspaceFolder` & `providerId` are provided we will not prompt the user to select a provider, nor folder, nor an interpreter. + */ +export interface CreateEnvironmentOptionsInternal { + workspaceFolder?: WorkspaceFolder; + providerId?: string; + interpreter?: string; +} diff --git a/src/client/pythonEnvironments/discovery/globalenv.ts b/src/client/pythonEnvironments/discovery/globalenv.ts deleted file mode 100644 index 3d60742e2d25..000000000000 --- a/src/client/pythonEnvironments/discovery/globalenv.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { logVerbose } from '../../logging'; -import { InterpreterType } 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 InterpreterType.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 deleted file mode 100644 index 6529b65d59c6..000000000000 --- a/src/client/pythonEnvironments/discovery/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index f1d259671b46..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/helpers.ts +++ /dev/null @@ -1,97 +0,0 @@ -import * as fsapi from 'fs-extra'; -import { inject } 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 { IPipEnvServiceHelper } from '../../../interpreter/locators/types'; -import { InterpreterType, PythonInterpreter } 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.listdir):', err); - return [] as string[]; - } -} - -export class InterpreterLocatorHelper { - constructor( - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IPipEnvServiceHelper) private readonly pipEnvServiceHelper: IPipEnvServiceHelper - ) {} - public async mergeInterpreters(interpreters: PythonInterpreter[]): Promise { - const items = interpreters - .map((item) => { - return { ...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.type === InterpreterType.Unknown && current.type !== InterpreterType.Unknown) { - existingItem.type = current.type; - } - const props: (keyof PythonInterpreter)[] = [ - '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.type = InterpreterType.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 deleted file mode 100644 index 40591b183d5c..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/index.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { inject } from 'inversify'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import { traceDecorators } from '../../../common/logger'; -import { IPlatformService } from '../../../common/platform/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 { PythonInterpreter } 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. - */ -export class PythonInterpreterLocatorService { - 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(); - 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 deleted file mode 100644 index 37ad078060cf..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/progressService.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject } 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 { IInterpreterLocatorService } from '../../../interpreter/contracts'; -import { IServiceContainer } from '../../../ioc/types'; -import { PythonInterpreter } from '../../info'; - -export class InterpreterLocatorProgressService { - 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 deleted file mode 100644 index d0dd4d9b3f7e..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/KnownPathsService.ts +++ /dev/null @@ -1,113 +0,0 @@ -// tslint:disable:no-require-imports no-var-requires no-unnecessary-callback-wrapper -import { inject } 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 { InterpreterType, PythonInterpreter } from '../../../info'; -import { lookForInterpretersInDirectory } from '../helpers'; -import { CacheableLocatorService } from './cacheableLocatorService'; -const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); - -/** - * Locates "known" paths. - */ -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 PythonInterpreter), - path: interpreter, - type: InterpreterType.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([]))); - } -} - -export class KnownSearchPathsForInterpreters { - 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 deleted file mode 100644 index a7a68da26e3a..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/baseVirtualEnvService.ts +++ /dev/null @@ -1,97 +0,0 @@ -// tslint:disable:no-unnecessary-callback-wrapper no-require-imports no-var-requires - -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 { InterpreterType, PythonInterpreter } from '../../../info'; -import { lookForInterpretersInDirectory } from '../helpers'; -import { CacheableLocatorService } from './cacheableLocatorService'; -const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); - -export class BaseVirtualEnvService extends CacheableLocatorService { - private readonly virtualEnvMgr: IVirtualEnvironmentManager; - private readonly helper: IInterpreterHelper; - private readonly fileSystem: IFileSystem; - public constructor( - private searchPathsProvider: IVirtualEnvironmentsSearchPathProvider, - serviceContainer: IServiceContainer, - name: string, - cachePerWorkspace: boolean = 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 PythonInterpreter[]; - }); - } - 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 PythonInterpreter), - envName: virtualEnvName, - type: type! as InterpreterType - }; - }); - } -} diff --git a/src/client/pythonEnvironments/discovery/locators/services/cacheableLocatorService.ts b/src/client/pythonEnvironments/discovery/locators/services/cacheableLocatorService.ts deleted file mode 100644 index 50883258cbbf..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/cacheableLocatorService.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-any - -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 { PythonInterpreter } 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); - } - } -} - -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( - private readonly name: string, - protected readonly serviceContainer: IServiceContainer, - 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); - } else { - return persistentFactory.createGlobalPersistentState(cacheKey, undefined as any); - } - } - protected getCachedInterpreters(resource?: Uri): PythonInterpreter[] | undefined { - const persistence = this.createPersistenceStore(resource); - if (!Array.isArray(persistence.value)) { - return; - } - return persistence.value.map((item) => { - return { - ...item, - cachedEntry: true - }; - }); - } - protected async cacheInterpreters(interpreters: PythonInterpreter[], 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 deleted file mode 100644 index 87b5478e2b4c..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/conda.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { InterpreterType, PythonInterpreter } 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 PythonInterpreter), - path: pythonPath, - companyDisplayName: AnacondaCompanyName, - type: InterpreterType.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 deleted file mode 100644 index 2af93d0d7f08..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/condaEnvFileService.ts +++ /dev/null @@ -1,128 +0,0 @@ -// 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 } 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 { InterpreterType, PythonInterpreter } from '../../../info'; -import { CacheableLocatorService } from './cacheableLocatorService'; -import { AnacondaCompanyName } from './conda'; - -/** - * Locate conda env interpreters based on the "conda environments file". - */ -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 PythonInterpreter[]; - } - } - - /** - * 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 PythonInterpreter), - path: interpreter, - companyDisplayName: AnacondaCompanyName, - type: InterpreterType.Conda, - envPath: environmentPath, - envName - }; - } -} diff --git a/src/client/pythonEnvironments/discovery/locators/services/condaEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/condaEnvService.ts deleted file mode 100644 index 0c9f232ea19a..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/condaEnvService.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject } 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 { PythonInterpreter } from '../../../info'; -import { CacheableLocatorService } from './cacheableLocatorService'; -import { parseCondaInfo } from './conda'; - -/** - * Locates conda env interpreters based on the conda service's info. - */ -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 deleted file mode 100644 index 2bb735b4a7e1..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/condaHelper.ts +++ /dev/null @@ -1,146 +0,0 @@ -// 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; - } else { - return `${displayName} : ${AnacondaDisplayName}`; - } - } else { - 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 deleted file mode 100644 index 05a4fac6f1a0..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/condaService.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { inject, 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 { IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE } from '../../../../interpreter/contracts'; -import { InterpreterType, PythonInterpreter } 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. - */ -export class CondaService { - 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.isWindows; - 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(interpreter: PythonInterpreter) { - return ( - interpreter.type === InterpreterType.Conda || - (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || - (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('ANACONDA') >= - 0 || - (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= - 0 - ); - } - - /** - * Return the highest Python version from the given list. - */ - private getLatestVersion(interpreters: PythonInterpreter[]) { - 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 deleted file mode 100644 index 849b318cb981..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/currentPathService.ts +++ /dev/null @@ -1,144 +0,0 @@ -// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation no-unnecessary-callback-wrapper -import { inject } 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 { InterpreterType, PythonInterpreter } 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). - */ -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 PythonInterpreter), - path: pythonPath, - type: details.type ? details.type : InterpreterType.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. - } - } -} - -export class PythonInPathCommandProvider { - constructor(@inject(IPlatformService) private readonly platform: IPlatformService) {} - public getCommands(): { command: string; args?: string[] }[] { - const paths = ['python3.7', 'python3.6', 'python3', 'python2', 'python'].map((item) => { - return { command: item }; - }); - if (this.platform.osType !== OSType.Windows) { - return paths; - } - - const versions = ['3.7', '3.6', '3', '2']; - return paths.concat( - versions.map((version) => { - return { command: 'py', args: [`-${version}`] }; - }) - ); - } -} diff --git a/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService.ts deleted file mode 100644 index 205f3f54fb53..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, 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'); - -export class GlobalVirtualEnvService extends BaseVirtualEnvService { - public constructor( - @inject(IVirtualEnvironmentsSearchPathProvider) - @named('global') - globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - super(globalVirtualEnvPathProvider, serviceContainer, 'VirtualEnvService'); - } -} - -export class GlobalVirtualEnvironmentsSearchPathProvider { - 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 deleted file mode 100644 index 50aa5f604bf6..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/hashProvider.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject } from 'inversify'; -import { IFileSystem } from '../../../../common/platform/types'; - -export class InterpreterHashProvider { - 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 deleted file mode 100644 index 32092b15aee0..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/hashProviderFactory.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject } from 'inversify'; -import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../../common/types'; -import { - IInterpreterHashProvider, - IWindowsStoreHashProvider, - IWindowsStoreInterpreter -} from '../../../../interpreter/locators/types'; - -export class InterpreterHashProviderFactory { - constructor( - @inject(IConfigurationService) private readonly configService: IConfigurationService, - @inject(IWindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter, - @inject(IWindowsStoreHashProvider) private readonly windowsStoreHashProvider: IWindowsStoreHashProvider, - @inject(IInterpreterHashProvider) 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 deleted file mode 100644 index f80b6f359d56..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/interpreterFilter.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { PythonInterpreter } from '../../../info'; -import { isRestrictedWindowsStoreInterpreterPath } from './windowsStoreInterpreter'; - -export function isHiddenInterpreter(interpreter: PythonInterpreter): 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 deleted file mode 100644 index acfeed9b624d..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject } 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, - IInterpreterWatcherRegistry, - WORKSPACE_VIRTUAL_ENV_SERVICE -} from '../../../../interpreter/contracts'; -import { IServiceContainer } from '../../../../ioc/types'; - -export class InterpreterWatcherBuilder { - 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( - IInterpreterWatcherRegistry, - 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 deleted file mode 100644 index cb03dca08734..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/pipEnvService.ts +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject } 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 { InterpreterType, PythonInterpreter } from '../../../info'; -import { GetInterpreterLocatorOptions } from '../types'; -import { CacheableLocatorService } from './cacheableLocatorService'; - -const pipEnvFileNameVariable = 'PIPENV_PIPFILE'; - -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 PythonInterpreter), - path: interpreterPath, - type: InterpreterType.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; - } - } - } - - 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 deleted file mode 100644 index 262c1dfbb446..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/pipEnvServiceHelper.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject } 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 }; - -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 deleted file mode 100644 index 2c145c12f5d5..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/windowsRegistryService.ts +++ /dev/null @@ -1,208 +0,0 @@ -// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation - -import { inject } 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 { InterpreterType, PythonInterpreter } from '../../../info'; -import { parsePythonVersion } from '../../../info/pythonVersion'; -import { CacheableLocatorService } from './cacheableLocatorService'; -import { AnacondaCompanyName, AnacondaCompanyNames } from './conda'; -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; -}; - -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(IWindowsStoreInterpreter) 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() : []; - } - 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) => { - return 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) => { - return { 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 PythonInterpreter), - 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, - type: this.windowsStoreInterpreter.isWindowsStoreInterpreter(executablePath) - ? InterpreterType.WindowsStore - : InterpreterType.Unknown - } as PythonInterpreter; - }) - .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 deleted file mode 100644 index 0943552b999b..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter.ts +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject } 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 { 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} - */ -export class WindowsStoreInterpreter { - 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/workspaceVirtualEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService.ts deleted file mode 100644 index 62e3fb4edaa8..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-require-imports - -import { inject, named } from 'inversify'; -import * as path from 'path'; -import untildify = require('untildify'); -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'; - -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)]; - } -} - -export class WorkspaceVirtualEnvironmentsSearchPathProvider { - 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).venvPath; - 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 deleted file mode 100644 index 844d3fde4c70..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvWatcherService.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject } 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'; - -const maxTimeToWaitForEnvCreation = 60_000; -const timeToPollForEnvCreation = 2_000; - -export class WorkspaceVirtualEnvWatcherService { - 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 deleted file mode 100644 index ffcace4f8022..000000000000 --- a/src/client/pythonEnvironments/discovery/locators/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 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 deleted file mode 100644 index 44904a8f4bfe..000000000000 --- a/src/client/pythonEnvironments/discovery/subenv.ts +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { InterpreterType } 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 !== InterpreterType.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); - } else { - 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 InterpreterType.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 ? InterpreterType.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 InterpreterType.Pipenv; - } - return undefined; - }; -} diff --git a/src/client/pythonEnvironments/exec.ts b/src/client/pythonEnvironments/exec.ts index 729e04b6c9a7..bd07a2a6192c 100644 --- a/src/client/pythonEnvironments/exec.ts +++ b/src/client/pythonEnvironments/exec.ts @@ -23,7 +23,11 @@ export type PythonExecInfo = { * @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 { +export function buildPythonExecInfo( + python: string | string[], + pythonArgs?: string[], + pythonExecutable?: string, +): PythonExecInfo { if (Array.isArray(python)) { const args = python.slice(1); if (pythonArgs) { @@ -33,18 +37,15 @@ export function buildPythonExecInfo(python: string | string[], pythonArgs?: stri 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] - }; - } else { - return { - command: python, - args: pythonArgs || [], - python: [python], - pythonExecutable: python + pythonExecutable: pythonExecutable ?? python[python.length - 1], }; } + return { + command: python, + args: pythonArgs || [], + python: [python], + pythonExecutable: python, + }; } /** @@ -58,15 +59,13 @@ export function copyPythonExecInfo(orig: PythonExecInfo, extraPythonArgs?: strin command: orig.command, args: [...orig.args], python: [...orig.python], - pythonExecutable: orig.pythonExecutable + 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]; + info.pythonExecutable = info.python[info.python.length - 1]; // Default case } return info; } diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts new file mode 100644 index 000000000000..299dfab59132 --- /dev/null +++ b/src/client/pythonEnvironments/index.ts @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; +import { cloneDeep } from 'lodash'; +import { getGlobalStorage, IPersistentStorage } from '../common/persistentState'; +import { getOSType, OSType } from '../common/utils/platform'; +import { ActivationResult, ExtensionState } from '../components'; +import { PythonEnvInfo } from './base/info'; +import { BasicEnvInfo, IDiscoveryAPI, ILocator } from './base/locator'; +import { PythonEnvsReducer } from './base/locators/composite/envsReducer'; +import { PythonEnvsResolver } from './base/locators/composite/envsResolver'; +import { WindowsPathEnvVarLocator } from './base/locators/lowLevel/windowsKnownPathsLocator'; +import { WorkspaceVirtualEnvironmentLocator } from './base/locators/lowLevel/workspaceVirtualEnvLocator'; +import { + initializeExternalDependencies as initializeLegacyExternalDependencies, + normCasePath, +} from './common/externalDependencies'; +import { ExtensionLocators, WatchRootsArgs, WorkspaceLocators } from './base/locators/wrappers'; +import { CustomVirtualEnvironmentLocator } from './base/locators/lowLevel/customVirtualEnvLocator'; +import { CondaEnvironmentLocator } from './base/locators/lowLevel/condaLocator'; +import { GlobalVirtualEnvironmentLocator } from './base/locators/lowLevel/globalVirtualEnvronmentLocator'; +import { PosixKnownPathsLocator } from './base/locators/lowLevel/posixKnownPathsLocator'; +import { PyenvLocator } from './base/locators/lowLevel/pyenvLocator'; +import { WindowsRegistryLocator } from './base/locators/lowLevel/windowsRegistryLocator'; +import { MicrosoftStoreLocator } from './base/locators/lowLevel/microsoftStoreLocator'; +import { getEnvironmentInfoService } from './base/info/environmentInfoService'; +import { registerNewDiscoveryForIOC } from './legacyIOC'; +import { PoetryLocator } from './base/locators/lowLevel/poetryLocator'; +import { HatchLocator } from './base/locators/lowLevel/hatchLocator'; +import { createPythonEnvironments } from './api'; +import { + createCollectionCache as createCache, + IEnvsCollectionCache, +} from './base/locators/composite/envsCollectionCache'; +import { EnvsCollectionService } from './base/locators/composite/envsCollectionService'; +import { IDisposable } from '../common/types'; +import { traceError } from '../logging'; +import { ActiveStateLocator } from './base/locators/lowLevel/activeStateLocator'; +import { CustomWorkspaceLocator } from './base/locators/lowLevel/customWorkspaceLocator'; +import { PixiLocator } from './base/locators/lowLevel/pixiLocator'; +import { getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { getNativePythonFinder } from './base/locators/common/nativePythonFinder'; +import { createNativeEnvironmentsApi } from './nativeAPI'; +import { useEnvExtension } from '../envExt/api.internal'; +import { createEnvExtApi } from '../envExt/envExtApi'; + +const PYTHON_ENV_INFO_CACHE_KEY = 'PYTHON_ENV_INFO_CACHEv2'; + +export function shouldUseNativeLocator(): boolean { + const config = getConfiguration('python'); + return config.get('locator', 'js') === 'native'; +} + +/** + * Set up the Python environments component (during extension activation).' + */ +export async function initialize(ext: ExtensionState): Promise { + // Set up the legacy IOC container before api is created. + initializeLegacyExternalDependencies(ext.legacyIOC.serviceContainer); + + if (useEnvExtension()) { + const api = await createEnvExtApi(ext.disposables); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; + } + + if (shouldUseNativeLocator()) { + const finder = getNativePythonFinder(ext.context); + const api = createNativeEnvironmentsApi(finder); + ext.disposables.push(api); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; + } + + const api = await createPythonEnvironments(() => createLocator(ext)); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; +} + +/** + * Make use of the component (e.g. register with VS Code). + */ +export async function activate(api: IDiscoveryAPI, ext: ExtensionState): Promise { + /** + * Force an initial background refresh of the environments. + * + * Note API is ready to be queried only after a refresh has been triggered, and extension activation is + * blocked on API being ready. So if discovery was never triggered for a scope, we need to block + * extension activation on the "refresh trigger". + */ + const folders = vscode.workspace.workspaceFolders; + // Trigger discovery if environment cache is empty. + const wasTriggered = getGlobalStorage(ext.context, PYTHON_ENV_INFO_CACHE_KEY, []).get().length > 0; + if (!wasTriggered) { + api.triggerRefresh().ignoreErrors(); + folders?.forEach(async (folder) => { + const wasTriggeredForFolder = getGlobalStorage( + ext.context, + `PYTHON_WAS_DISCOVERY_TRIGGERED_${normCasePath(folder.uri.fsPath)}`, + false, + ); + await wasTriggeredForFolder.set(true); + }); + } else { + // Figure out which workspace folders need to be activated if any. + folders?.forEach(async (folder) => { + const wasTriggeredForFolder = getGlobalStorage( + ext.context, + `PYTHON_WAS_DISCOVERY_TRIGGERED_${normCasePath(folder.uri.fsPath)}`, + false, + ); + if (!wasTriggeredForFolder.get()) { + api.triggerRefresh({ + searchLocations: { roots: [folder.uri], doNotIncludeNonRooted: true }, + }).ignoreErrors(); + await wasTriggeredForFolder.set(true); + } + }); + } + + return { + fullyReady: Promise.resolve(), + }; +} + +/** + * Get the locator to use in the component. + */ +async function createLocator( + ext: ExtensionState, + // This is shared. +): Promise { + // Create the low-level locators. + const locators: ILocator = new ExtensionLocators( + // Here we pull the locators together. + createNonWorkspaceLocators(ext), + createWorkspaceLocator(ext), + ); + + // Create the env info service used by ResolvingLocator and CachingLocator. + const envInfoService = getEnvironmentInfoService(ext.disposables); + + // Build the stack of composite locators. + const reducer = new PythonEnvsReducer(locators); + const resolvingLocator = new PythonEnvsResolver( + reducer, + // These are shared. + envInfoService, + ); + const caching = new EnvsCollectionService( + await createCollectionCache(ext), + // This is shared. + resolvingLocator, + shouldUseNativeLocator(), + ); + return caching; +} + +function createNonWorkspaceLocators(ext: ExtensionState): ILocator[] { + const locators: (ILocator & Partial)[] = []; + locators.push( + // OS-independent locators go here. + new PyenvLocator(), + new CondaEnvironmentLocator(), + new ActiveStateLocator(), + new GlobalVirtualEnvironmentLocator(), + new CustomVirtualEnvironmentLocator(), + ); + + if (getOSType() === OSType.Windows) { + locators.push( + // Windows specific locators go here. + new WindowsRegistryLocator(), + new MicrosoftStoreLocator(), + new WindowsPathEnvVarLocator(), + ); + } else { + locators.push( + // Linux/Mac locators go here. + new PosixKnownPathsLocator(), + ); + } + + const disposables = locators.filter((d) => d.dispose !== undefined) as IDisposable[]; + ext.disposables.push(...disposables); + return locators; +} + +function watchRoots(args: WatchRootsArgs): IDisposable { + const { initRoot, addRoot, removeRoot } = args; + + const folders = vscode.workspace.workspaceFolders; + if (folders) { + folders.map((f) => f.uri).forEach(initRoot); + } + + return vscode.workspace.onDidChangeWorkspaceFolders((event) => { + for (const root of event.removed) { + removeRoot(root.uri); + } + for (const root of event.added) { + addRoot(root.uri); + } + }); +} + +function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { + const locators = new WorkspaceLocators(watchRoots, [ + (root: vscode.Uri) => [ + new WorkspaceVirtualEnvironmentLocator(root.fsPath), + new PoetryLocator(root.fsPath), + new HatchLocator(root.fsPath), + new PixiLocator(root.fsPath), + new CustomWorkspaceLocator(root.fsPath), + ], + // Add an ILocator factory func here for each kind of workspace-rooted locator. + ]); + ext.disposables.push(locators); + return locators; +} + +function getFromStorage(storage: IPersistentStorage): PythonEnvInfo[] { + return storage.get().map((e) => { + if (e.searchLocation) { + if (typeof e.searchLocation === 'string') { + e.searchLocation = Uri.parse(e.searchLocation); + } else if ('scheme' in e.searchLocation && 'path' in e.searchLocation) { + e.searchLocation = Uri.parse(`${e.searchLocation.scheme}://${e.searchLocation.path}`); + } else { + traceError('Unexpected search location', JSON.stringify(e.searchLocation)); + } + } + return e; + }); +} + +function putIntoStorage(storage: IPersistentStorage, envs: PythonEnvInfo[]): Promise { + storage.set( + // We have to `cloneDeep()` here so that we don't overwrite the original `PythonEnvInfo` objects. + cloneDeep(envs).map((e) => { + if (e.searchLocation) { + // Make TS believe it is string. This is temporary. We need to serialize this in + // a custom way. + e.searchLocation = (e.searchLocation.toString() as unknown) as Uri; + } + return e; + }), + ); + return Promise.resolve(); +} + +async function createCollectionCache(ext: ExtensionState): Promise { + const storage = getGlobalStorage(ext.context, PYTHON_ENV_INFO_CACHE_KEY, []); + const cache = await createCache({ + get: () => getFromStorage(storage), + store: async (e) => putIntoStorage(storage, e), + }); + return cache; +} diff --git a/src/client/pythonEnvironments/info/executable.ts b/src/client/pythonEnvironments/info/executable.ts index f561a4cc077e..70c74329c49b 100644 --- a/src/client/pythonEnvironments/info/executable.ts +++ b/src/client/pythonEnvironments/info/executable.ts @@ -1,25 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { getExecutable as getPythonExecutableCommand } from '../../common/process/internal/python'; +import { getExecutable } from '../../common/process/internal/python'; +import { ShellExecFunc } from '../../common/process/types'; +import { traceError } from '../../logging'; 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 + * @param shellExec - 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); +export async function getExecutablePath(python: PythonExecInfo, shellExec: ShellExecFunc): Promise { + try { + const [args, parse] = getExecutable(); + 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.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); + const result = await shellExec(quoted, { timeout: 15000 }); + const executable = parse(result.stdout.trim()); + if (executable === '') { + throw new Error(`${quoted} resulted in empty stdout`); + } + return executable; + } catch (ex) { + traceError(ex); + return undefined; + } } diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 6b9aff7183ed..08310767914a 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -3,31 +3,54 @@ 'use strict'; -import * as semver from 'semver'; import { Architecture } from '../../common/utils/platform'; +import { PythonEnvType } from '../base/info'; import { PythonVersion } from './pythonVersion'; /** * The supported Python environment types. */ -export enum InterpreterType { +export enum EnvironmentType { Unknown = 'Unknown', Conda = 'Conda', VirtualEnv = 'VirtualEnv', Pipenv = 'PipEnv', Pyenv = 'Pyenv', Venv = 'Venv', - WindowsStore = 'WindowsStore' + MicrosoftStore = 'MicrosoftStore', + Poetry = 'Poetry', + Hatch = 'Hatch', + Pixi = 'Pixi', + VirtualEnvWrapper = 'VirtualEnvWrapper', + ActiveState = 'ActiveState', + Global = 'Global', + System = 'System', } +/** + * These envs are only created for a specific workspace, which we're able to detect. + */ +export const workspaceVirtualEnvTypes = [EnvironmentType.Poetry, EnvironmentType.Pipenv, EnvironmentType.Pixi]; -type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown'; +export const virtualEnvTypes = [ + ...workspaceVirtualEnvTypes, + EnvironmentType.Hatch, // This is also a workspace virtual env, but we're not treating it as such as of today. + EnvironmentType.Venv, + EnvironmentType.VirtualEnvWrapper, + EnvironmentType.Conda, + EnvironmentType.VirtualEnv, +]; /** - * The components of a Python version. - * - * These match the elements of `sys.version_info`. + * The IModuleInstaller implementations. */ -export type PythonVersionInfo = [number, number, number, ReleaseLevel]; +export enum ModuleInstallerType { + Unknown = 'Unknown', + Conda = 'Conda', + Pip = 'Pip', + Poetry = 'Poetry', + Pipenv = 'Pipenv', + Pixi = 'Pixi', +} /** * Details about a Python runtime. @@ -42,7 +65,7 @@ export type PythonVersionInfo = [number, number, number, ReleaseLevel]; export type InterpreterInformation = { path: string; version?: PythonVersion; - sysVersion: string; + sysVersion?: string; architecture: Architecture; sysPrefix: string; pipEnvWorkspaceFolder?: string; @@ -53,59 +76,66 @@ export type InterpreterInformation = { * * @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 envType - 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 + * @prop type - the type of Python environment, if applicable */ // Note that "cachedEntry" is specific to the caching machinery // and doesn't really belong here. -export type PythonInterpreter = InterpreterInformation & { +export type PythonEnvironment = InterpreterInformation & { + id?: string; companyDisplayName?: string; displayName?: string; - type: InterpreterType; + detailedDisplayName?: string; + envType: EnvironmentType; envName?: string; envPath?: string; cachedEntry?: boolean; + type?: PythonEnvType; }; /** * Convert the Python environment type to a user-facing name. */ -export function getInterpreterTypeName(interpreterType: InterpreterType) { - switch (interpreterType) { - case InterpreterType.Conda: { +export function getEnvironmentTypeName(environmentType: EnvironmentType): string { + switch (environmentType) { + case EnvironmentType.Conda: { return 'conda'; } - case InterpreterType.Pipenv: { - return 'pipenv'; + case EnvironmentType.Pipenv: { + return 'Pipenv'; } - case InterpreterType.Pyenv: { + case EnvironmentType.Pyenv: { return 'pyenv'; } - case InterpreterType.Venv: { + case EnvironmentType.Venv: { return 'venv'; } - case InterpreterType.VirtualEnv: { + case EnvironmentType.VirtualEnv: { return 'virtualenv'; } + case EnvironmentType.MicrosoftStore: { + return 'Microsoft Store'; + } + case EnvironmentType.Poetry: { + return 'Poetry'; + } + case EnvironmentType.Hatch: { + return 'Hatch'; + } + case EnvironmentType.Pixi: { + return 'pixi'; + } + case EnvironmentType.VirtualEnvWrapper: { + return 'virtualenvwrapper'; + } + case EnvironmentType.ActiveState: { + return 'ActiveState'; + } default: { return ''; } } } - -/** - * Build a version-sorted list from the given one, with lowest first. - */ -export function sortInterpreters(interpreters: PythonInterpreter[]): PythonInterpreter[] { - 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 index da6e58e10265..8fe9bc7d49a8 100644 --- a/src/client/pythonEnvironments/info/interpreter.ts +++ b/src/client/pythonEnvironments/info/interpreter.ts @@ -1,11 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { SemVer } from 'semver'; import { InterpreterInformation } from '.'; -import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../common/process/internal/scripts'; +import { + interpreterInfo as getInterpreterInfoCommand, + InterpreterInfoJson, +} from '../../common/process/internal/scripts'; +import { ShellExecFunc } from '../../common/process/types'; +import { replaceAll } from '../../common/stringUtils'; import { Architecture } from '../../common/utils/platform'; import { copyPythonExecInfo, PythonExecInfo } from '../exec'; -import { parsePythonVersion } from './pythonVersion'; /** * Compose full interpreter information based on the given data. @@ -15,25 +20,35 @@ import { parsePythonVersion } from './pythonVersion'; * @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]}`; +function extractInterpreterInfo(python: string, raw: InterpreterInfoJson): InterpreterInformation { + let rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}`; + // We only need additional version details if the version is 'alpha', 'beta' or 'candidate'. + // This restriction is needed to avoid sending any PII if this data is used with telemetry. + // With custom builds of python it is possible that release level and values after that can + // contain PII. + if (raw.versionInfo[3] !== undefined && ['alpha', 'beta', 'candidate'].includes(raw.versionInfo[3])) { + rawVersion = `${rawVersion}-${raw.versionInfo[3]}`; + if (raw.versionInfo[4] !== undefined) { + let serial = -1; + try { + serial = parseInt(`${raw.versionInfo[4]}`, 10); + } catch (ex) { + serial = -1; + } + rawVersion = serial >= 0 ? `${rawVersion}${serial}` : rawVersion; + } + } return { architecture: raw.is64Bit ? Architecture.x64 : Architecture.x86, path: python, - version: parsePythonVersion(rawVersion), + version: new SemVer(rawVersion), sysVersion: raw.sysVersion, - sysPrefix: raw.sysPrefix + sysPrefix: raw.sysPrefix, }; } -type ShellExecResult = { - stdout: string; - stderr?: string; -}; -type ShellExecFunc = (command: string, timeout: number) => Promise; - type Logger = { - info(msg: string): void; + verbose(msg: string): void; error(msg: string): void; }; @@ -47,14 +62,14 @@ type Logger = { export async function getInterpreterInfo( python: PythonExecInfo, shellExec: ShellExecFunc, - logger?: Logger + 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('\\', '\\\\')}"`), ''); + const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${replaceAll(c, '\\', '\\\\')}"`), ''); // Try shell execing the command, followed by the arguments. This will make node kill the process if it // takes too long. @@ -62,16 +77,18 @@ export async function getInterpreterInfo( // 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); + const result = await shellExec(quoted, { timeout: 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}`); + logger.verbose(`Found interpreter for ${argv}`); + } + if (!json) { + return undefined; } return extractInterpreterInfo(python.pythonExecutable, json); } diff --git a/src/client/pythonEnvironments/info/pythonVersion.ts b/src/client/pythonEnvironments/info/pythonVersion.ts index 459dedb6ad9d..d61fcf14db4d 100644 --- a/src/client/pythonEnvironments/info/pythonVersion.ts +++ b/src/client/pythonEnvironments/info/pythonVersion.ts @@ -1,10 +1,6 @@ // 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. * @@ -30,71 +26,10 @@ export type PythonVersion = { 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); -} - -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); +export function isStableVersion(version: PythonVersion): boolean { + // A stable version is one that has no prerelease tags. + return ( + version.prerelease.length === 0 && + (version.build.length === 0 || (version.build.length === 1 && version.build[0] === 'final')) + ); } diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 6d39f540171f..49df2ee03f21 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -1,552 +1,296 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-use-before-declare max-classes-per-file - -import { inject, injectable, named, optional } from 'inversify'; -import { SemVer } from 'semver'; -import { Disposable, Event, Uri } from 'vscode'; -import { IWorkspaceService } from '../common/application/types'; -import { IFileSystem, IPlatformService, IRegistry } from '../common/platform/types'; -import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory, Resource } from '../common/types'; +import { injectable } from 'inversify'; +import { intersection } from 'lodash'; +import * as vscode from 'vscode'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { Resource } from '../common/types'; +import { IComponentAdapter, ICondaService, PythonEnvironmentsChangedEvent } from '../interpreter/contracts'; +import { IServiceManager } from '../ioc/types'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvSource } from './base/info'; import { - CONDA_ENV_FILE_SERVICE, - CONDA_ENV_SERVICE, - CURRENT_PATH_SERVICE, - GLOBAL_VIRTUAL_ENV_SERVICE, - ICondaService, - IInterpreterHelper, - IInterpreterLocatorHelper, - IInterpreterLocatorProgressService, - IInterpreterLocatorService, - IInterpreterWatcher, - IInterpreterWatcherBuilder, - IInterpreterWatcherRegistry, - IKnownSearchPathsForInterpreters, - INTERPRETER_LOCATOR_SERVICE, - IPipEnvService, - IVirtualEnvironmentsSearchPathProvider, - KNOWN_PATH_SERVICE, - PIPENV_SERVICE, - WINDOWS_REGISTRY_SERVICE, - WORKSPACE_VIRTUAL_ENV_SERVICE -} from '../interpreter/contracts'; -import { - IInterpreterHashProvider, - IInterpreterHashProviderFactory, - IPipEnvServiceHelper, - IPythonInPathCommandProvider, - IWindowsStoreHashProvider, - IWindowsStoreInterpreter -} from '../interpreter/locators/types'; -import { IServiceContainer, IServiceManager } from '../ioc/types'; -import { PythonInterpreterLocatorService } from './discovery/locators'; -import { InterpreterLocatorHelper } from './discovery/locators/helpers'; -import { InterpreterLocatorProgressService } from './discovery/locators/progressService'; -import { CondaEnvironmentInfo, CondaInfo } from './discovery/locators/services/conda'; -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 { InterpreterHashProviderFactory } 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 { GetInterpreterLocatorOptions } from './discovery/locators/types'; -import { PythonInterpreter } from './info'; - -export function registerForIOC(serviceManager: IServiceManager) { - serviceManager.addSingleton(IInterpreterLocatorHelper, InterpreterLocatorHelperProxy); - serviceManager.addSingleton( - IInterpreterLocatorService, - PythonInterpreterLocatorServiceProxy, - INTERPRETER_LOCATOR_SERVICE - ); - serviceManager.addSingleton( - IInterpreterLocatorProgressService, - InterpreterLocatorProgressServiceProxy - ); - serviceManager.addSingleton( - IInterpreterLocatorService, - CondaEnvFileServiceProxy, - CONDA_ENV_FILE_SERVICE - ); - serviceManager.addSingleton( - IInterpreterLocatorService, - CondaEnvServiceProxy, - CONDA_ENV_SERVICE - ); - serviceManager.addSingleton( - IInterpreterLocatorService, - CurrentPathServiceProxy, - CURRENT_PATH_SERVICE - ); - serviceManager.addSingleton( - IInterpreterLocatorService, - GlobalVirtualEnvServiceProxy, - GLOBAL_VIRTUAL_ENV_SERVICE - ); - serviceManager.addSingleton( - IInterpreterLocatorService, - WorkspaceVirtualEnvServiceProxy, - WORKSPACE_VIRTUAL_ENV_SERVICE - ); - serviceManager.addSingleton( - IInterpreterLocatorService, - PipEnvLocatorServiceProxy, - PIPENV_SERVICE - ); - - serviceManager.addSingleton( - IInterpreterLocatorService, - WindowsRegistryServiceProxy, - WINDOWS_REGISTRY_SERVICE - ); - serviceManager.addSingleton( - IInterpreterLocatorService, - KnownPathsServiceProxy, - KNOWN_PATH_SERVICE - ); - serviceManager.addSingleton(ICondaService, CondaServiceProxy); - serviceManager.addSingleton(IPipEnvService, PipEnvServiceProxy); - serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelperProxy); - serviceManager.addSingleton( - IPythonInPathCommandProvider, - PythonInPathCommandProviderProxy - ); - - serviceManager.add( - IInterpreterWatcherRegistry, - WorkspaceVirtualEnvWatcherServiceProxy, - WORKSPACE_VIRTUAL_ENV_SERVICE - ); - serviceManager.addSingleton(IWindowsStoreInterpreter, WindowsStoreInterpreterProxy); - serviceManager.addSingleton(IWindowsStoreHashProvider, WindowsStoreInterpreterProxy); - serviceManager.addSingleton(IInterpreterHashProvider, InterpreterHashProviderProxy); - serviceManager.addSingleton( - IInterpreterHashProviderFactory, - InterpreterHashProviderFactoryProxy - ); - serviceManager.addSingleton( - IVirtualEnvironmentsSearchPathProvider, - GlobalVirtualEnvironmentsSearchPathProviderProxy, - 'global' - ); - serviceManager.addSingleton( - IVirtualEnvironmentsSearchPathProvider, - WorkspaceVirtualEnvironmentsSearchPathProviderProxy, - 'workspace' - ); - serviceManager.addSingleton( - IKnownSearchPathsForInterpreters, - KnownSearchPathsForInterpretersProxy - ); - serviceManager.addSingleton(IInterpreterWatcherBuilder, InterpreterWatcherBuilderProxy); -} + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + PythonLocatorQuery, + TriggerRefreshOptions, +} from './base/locator'; +import { isMacDefaultPythonPath } from './common/environmentManagers/macDefault'; +import { isParentPath } from './common/externalDependencies'; +import { EnvironmentType, PythonEnvironment } from './info'; +import { toSemverLikeVersion } from './base/info/pythonVersion'; +import { PythonVersion } from './info/pythonVersion'; +import { createDeferred } from '../common/utils/async'; +import { PythonEnvCollectionChangedEvent } from './base/watcher'; +import { asyncFilter } from '../common/utils/arrayUtils'; +import { CondaEnvironmentInfo, isCondaEnvironment } from './common/environmentManagers/conda'; +import { isMicrosoftStoreEnvironment } from './common/environmentManagers/microsoftStoreEnv'; +import { CondaService } from './common/environmentManagers/condaService'; +import { traceError, traceVerbose } from '../logging'; -@injectable() -class InterpreterLocatorHelperProxy implements IInterpreterLocatorHelper { - private readonly impl: IInterpreterLocatorHelper; - constructor( - @inject(IFileSystem) fs: IFileSystem, - @inject(IPipEnvServiceHelper) pipEnvServiceHelper: IPipEnvServiceHelper - ) { - this.impl = new InterpreterLocatorHelper(fs, pipEnvServiceHelper); - } - public async mergeInterpreters(interpreters: PythonInterpreter[]): Promise { - return this.impl.mergeInterpreters(interpreters); - } +const convertedKinds = new Map( + Object.entries({ + [PythonEnvKind.OtherGlobal]: EnvironmentType.Global, + [PythonEnvKind.System]: EnvironmentType.System, + [PythonEnvKind.MicrosoftStore]: EnvironmentType.MicrosoftStore, + [PythonEnvKind.Pyenv]: EnvironmentType.Pyenv, + [PythonEnvKind.Conda]: EnvironmentType.Conda, + [PythonEnvKind.VirtualEnv]: EnvironmentType.VirtualEnv, + [PythonEnvKind.Pipenv]: EnvironmentType.Pipenv, + [PythonEnvKind.Poetry]: EnvironmentType.Poetry, + [PythonEnvKind.Hatch]: EnvironmentType.Hatch, + [PythonEnvKind.Pixi]: EnvironmentType.Pixi, + [PythonEnvKind.Venv]: EnvironmentType.Venv, + [PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper, + [PythonEnvKind.ActiveState]: EnvironmentType.ActiveState, + }), +); + +export function convertEnvInfoToPythonEnvironment(info: PythonEnvInfo): PythonEnvironment { + return convertEnvInfo(info); } -@injectable() -class InterpreterLocatorProgressServiceProxy implements IInterpreterLocatorProgressService { - private readonly impl: IInterpreterLocatorProgressService; - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) disposables: Disposable[] - ) { - this.impl = new InterpreterLocatorProgressService(serviceContainer, disposables); - } +function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { + const { name, location, executable, arch, kind, version, distro, id } = info; + const { filename, sysPrefix } = executable; + const env: PythonEnvironment = { + id, + sysPrefix, + envType: EnvironmentType.Unknown, + envName: name, + envPath: location, + path: filename, + architecture: arch, + }; - public get onRefreshing(): Event { - return this.impl.onRefreshing; - } - public get onRefreshed(): Event { - return this.impl.onRefreshed; - } - public register(): void { - this.impl.register(); + const envType = convertedKinds.get(kind); + if (envType !== undefined) { + env.envType = envType; } -} + // Otherwise it stays Unknown. -@injectable() -class InterpreterHashProviderFactoryProxy implements IInterpreterHashProviderFactory { - private readonly impl: IInterpreterHashProviderFactory; - constructor( - @inject(IConfigurationService) configService: IConfigurationService, - @inject(IWindowsStoreInterpreter) windowsStoreInterpreter: IWindowsStoreInterpreter, - @inject(IWindowsStoreHashProvider) windowsStoreHashProvider: IWindowsStoreHashProvider, - @inject(IInterpreterHashProvider) hashProvider: IInterpreterHashProvider - ) { - this.impl = new InterpreterHashProviderFactory( - configService, - windowsStoreInterpreter, - windowsStoreHashProvider, - hashProvider - ); - } - public async create(options: { pythonPath: string } | { resource: Uri }): Promise { - return this.impl.create(options); - } -} + if (version !== undefined) { + const { release, sysVersion } = version; + if (release === undefined) { + env.sysVersion = ''; + } else { + env.sysVersion = sysVersion; + } -@injectable() -class InterpreterHashProviderProxy implements IInterpreterHashProvider { - private readonly impl: IInterpreterHashProvider; - constructor(@inject(IFileSystem) fs: IFileSystem) { - this.impl = new InterpreterHashProvider(fs); + const semverLikeVersion: PythonVersion = toSemverLikeVersion(version); + env.version = semverLikeVersion; } - public async getInterpreterHash(pythonPath: string): Promise { - return this.impl.getInterpreterHash(pythonPath); + + if (distro !== undefined && distro.org !== '') { + env.companyDisplayName = distro.org; } -} + env.displayName = info.display; + env.detailedDisplayName = info.detailedDisplayName; + env.type = info.type; + // We do not worry about using distro.defaultDisplayName. + return env; +} @injectable() -class WindowsStoreInterpreterProxy implements IWindowsStoreInterpreter, IWindowsStoreHashProvider { - private readonly impl: IWindowsStoreInterpreter & IWindowsStoreHashProvider; +class ComponentAdapter implements IComponentAdapter { + private readonly changed = new vscode.EventEmitter(); + constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IPersistentStateFactory) persistentFactory: IPersistentStateFactory, - @inject(IFileSystem) fs: IFileSystem + // The adapter only wraps one thing: the component API. + private readonly api: IDiscoveryAPI, ) { - this.impl = new WindowsStoreInterpreter(serviceContainer, persistentFactory, fs); + this.api.onChanged((event) => { + this.changed.fire({ + type: event.type, + new: event.new ? convertEnvInfo(event.new) : undefined, + old: event.old ? convertEnvInfo(event.old) : undefined, + resource: event.searchLocation, + }); + }); } - public isWindowsStoreInterpreter(pythonPath: string): boolean { - return this.impl.isWindowsStoreInterpreter(pythonPath); - } - public isHiddenInterpreter(pythonPath: string): boolean { - return this.impl.isHiddenInterpreter(pythonPath); - } - public async getInterpreterHash(pythonPath: string): Promise { - return this.impl.getInterpreterHash(pythonPath); - } -} -@injectable() -class PythonInPathCommandProviderProxy implements IPythonInPathCommandProvider { - private readonly impl: IPythonInPathCommandProvider; - constructor(@inject(IPlatformService) platform: IPlatformService) { - this.impl = new PythonInPathCommandProvider(platform); - } - public getCommands(): { command: string; args?: string[] }[] { - return this.impl.getCommands(); + public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise { + return this.api.triggerRefresh(query, options); } -} -@injectable() -class KnownSearchPathsForInterpretersProxy implements IKnownSearchPathsForInterpreters { - private readonly impl: IKnownSearchPathsForInterpreters; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.impl = new KnownSearchPathsForInterpreters(serviceContainer); + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) { + return this.api.getRefreshPromise(options); } - public getSearchPaths(): string[] { - return this.impl.getSearchPaths(); - } -} -@injectable() -class WorkspaceVirtualEnvironmentsSearchPathProviderProxy implements IVirtualEnvironmentsSearchPathProvider { - private readonly impl: IVirtualEnvironmentsSearchPathProvider; - public constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.impl = new WorkspaceVirtualEnvironmentsSearchPathProvider(serviceContainer); - } - public async getSearchPaths(resource?: Uri): Promise { - return this.impl.getSearchPaths(resource); + public get onProgress() { + return this.api.onProgress; } -} -@injectable() -class GlobalVirtualEnvironmentsSearchPathProviderProxy implements IVirtualEnvironmentsSearchPathProvider { - private readonly impl: IVirtualEnvironmentsSearchPathProvider; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.impl = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + public get onChanged() { + return this.changed.event; } - public async getSearchPaths(resource?: Uri): Promise { - return this.impl.getSearchPaths(resource); - } -} -@injectable() -class PipEnvServiceProxy { - private readonly impl: IPipEnvService; - constructor(@inject(IInterpreterLocatorService) @named(PIPENV_SERVICE) proxy: IPipEnvService) { - // tslint:disable-next-line:no-any - const locator = (proxy as unknown) as any; - this.impl = locator.pipEnvService; - } - public async isRelatedPipEnvironment(dir: string, pythonPath: string): Promise { - return this.impl.isRelatedPipEnvironment(dir, pythonPath); - } - public get executable(): string { - return this.impl.executable; - } -} + // For use in VirtualEnvironmentPrompt.activate() -@injectable() -class PipEnvServiceHelperProxy implements IPipEnvServiceHelper { - private readonly impl: IPipEnvServiceHelper; - constructor( - @inject(IPersistentStateFactory) statefactory: IPersistentStateFactory, - @inject(IFileSystem) fs: IFileSystem - ) { - this.impl = new PipEnvServiceHelper(statefactory, fs); + // Call callback if an environment gets created within the resource provided. + public onDidCreate(resource: Resource, callback: () => void): vscode.Disposable { + const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : undefined; + return this.api.onChanged((e) => { + if (!workspaceFolder || !e.searchLocation) { + return; + } + traceVerbose(`Received event ${JSON.stringify(e)} file change event`); + if ( + e.type === FileChangeType.Created && + isParentPath(e.searchLocation.fsPath, workspaceFolder.uri.fsPath) + ) { + callback(); + } + }); } - public async getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string } | undefined> { - return this.impl.getPipEnvInfo(pythonPath); - } - public async trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise { - return this.impl.trackWorkspaceFolder(pythonPath, workspaceFolder); - } -} -@injectable() -class CondaServiceProxy implements ICondaService { - private readonly impl: ICondaService; - constructor( - @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, - @inject(IPlatformService) platform: IPlatformService, - @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IPersistentStateFactory) persistentStateFactory: IPersistentStateFactory, - @inject(IConfigurationService) configService: IConfigurationService, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IInterpreterLocatorService) - @named(WINDOWS_REGISTRY_SERVICE) - @optional() - registryLookupForConda?: IInterpreterLocatorService - ) { - this.impl = new CondaService( - processServiceFactory, - platform, - fileSystem, - persistentStateFactory, - configService, - disposableRegistry, - workspaceService, - registryLookupForConda - ); - } - public get condaEnvironmentsFile(): string | undefined { - return this.impl.condaEnvironmentsFile; - } - public async getCondaFile(): Promise { - return this.impl.getCondaFile(); - } - public async isCondaAvailable(): Promise { - return this.impl.isCondaAvailable(); - } - public async getCondaVersion(): Promise { - return this.impl.getCondaVersion(); - } - public async getCondaInfo(): Promise { - return this.impl.getCondaInfo(); + // Implements IInterpreterHelper + public async getInterpreterInformation(pythonPath: string): Promise | undefined> { + const env = await this.api.resolveEnv(pythonPath); + return env ? convertEnvInfo(env) : undefined; } - public async getCondaEnvironments(ignoreCache: boolean): Promise { - return this.impl.getCondaEnvironments(ignoreCache); - } - public getInterpreterPath(condaEnvironmentPath: string): string { - return this.impl.getInterpreterPath(condaEnvironmentPath); - } - public async getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise { - return this.impl.getCondaFileFromInterpreter(interpreterPath, envName); - } - public async isCondaEnvironment(interpreterPath: string): Promise { - return this.impl.isCondaEnvironment(interpreterPath); - } - public async getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined> { - return this.impl.getCondaEnvironment(interpreterPath); - } -} -@injectable() -class InterpreterWatcherBuilderProxy implements IInterpreterWatcherBuilder { - private readonly impl: IInterpreterWatcherBuilder; - constructor( - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - this.impl = new InterpreterWatcherBuilder(workspaceService, serviceContainer); + // eslint-disable-next-line class-methods-use-this + public async isMacDefaultPythonPath(pythonPath: string): Promise { + // While `ComponentAdapter` represents how the component would be used in the rest of the + // extension, we cheat here for the sake of performance. This is not a problem because when + // we start using the component's public API directly we will be dealing with `PythonEnvInfo` + // instead of just `pythonPath`. + return isMacDefaultPythonPath(pythonPath); } - public async getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise { - return this.impl.getWorkspaceVirtualEnvInterpreterWatcher(resource); - } -} -@injectable() -class WorkspaceVirtualEnvWatcherServiceProxy implements IInterpreterWatcherRegistry, Disposable { - private readonly impl: IInterpreterWatcherRegistry & Disposable; - constructor( - @inject(IDisposableRegistry) disposableRegistry: Disposable[], - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IPlatformService) platformService: IPlatformService, - @inject(IPythonExecutionFactory) pythonExecFactory: IPythonExecutionFactory - ) { - this.impl = new WorkspaceVirtualEnvWatcherService( - disposableRegistry, - workspaceService, - platformService, - pythonExecFactory - ); - } - public get onDidCreate(): Event { - return this.impl.onDidCreate; - } - public async register(resource: Resource): Promise { - return this.impl.register(resource); - } - public dispose() { - return this.impl.dispose(); + // Implements IInterpreterService + + // We use the same getInterpreters() here as for IInterpreterLocatorService. + public async getInterpreterDetails(pythonPath: string): Promise { + try { + const env = await this.api.resolveEnv(pythonPath); + if (!env) { + return undefined; + } + return convertEnvInfo(env); + } catch (ex) { + traceError(`Failed to resolve interpreter: ${pythonPath}`, ex); + return undefined; + } } -} -//=========================== -// locators + // Implements ICondaService -@injectable() -class BaseLocatorServiceProxy implements IInterpreterLocatorService { - constructor(protected readonly impl: IInterpreterLocatorService) {} - public dispose() { - this.impl.dispose(); - } - public get onLocating(): Event> { - return this.impl.onLocating; - } - public get hasInterpreters(): Promise { - return this.impl.hasInterpreters; - } - public get didTriggerInterpreterSuggestions(): boolean { - return this.impl.didTriggerInterpreterSuggestions as boolean; - } - public set didTriggerInterpreterSuggestions(value: boolean) { - this.impl.didTriggerInterpreterSuggestions = value; - } - public async getInterpreters(resource?: Uri, options?: GetInterpreterLocatorOptions): Promise { - return this.impl.getInterpreters(resource, options); + // eslint-disable-next-line class-methods-use-this + public async isCondaEnvironment(interpreterPath: string): Promise { + // While `ComponentAdapter` represents how the component would be used in the rest of the + // extension, we cheat here for the sake of performance. This is not a problem because when + // we start using the component's public API directly we will be dealing with `PythonEnvInfo` + // instead of just `pythonPath`. + return isCondaEnvironment(interpreterPath); } -} -@injectable() -class PythonInterpreterLocatorServiceProxy extends BaseLocatorServiceProxy { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(new PythonInterpreterLocatorService(serviceContainer)); - serviceContainer.get(IDisposableRegistry).push(this.impl); - } -} + public async getCondaEnvironment(interpreterPath: string): Promise { + if (!(await isCondaEnvironment(interpreterPath))) { + // Undefined is expected here when the env is not Conda env. + return undefined; + } -@injectable() -class CondaEnvFileServiceProxy extends BaseLocatorServiceProxy { - constructor( - @inject(IInterpreterHelper) helperService: IInterpreterHelper, - @inject(ICondaService) condaService: ICondaService, - @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - super(new CondaEnvFileService(helperService, condaService, fileSystem, serviceContainer)); - } -} + // The API getCondaEnvironment() is not called automatically, unless user attempts to install or activate environments + // So calling resolveEnv() which although runs python unnecessarily, is not that expensive here. + const env = await this.api.resolveEnv(interpreterPath); -@injectable() -class CondaEnvServiceProxy extends BaseLocatorServiceProxy { - constructor( - @inject(ICondaService) condaService: ICondaService, - @inject(IInterpreterHelper) helper: IInterpreterHelper, - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IFileSystem) fileSystem: IFileSystem - ) { - super(new CondaEnvService(condaService, helper, serviceContainer, fileSystem)); - } -} + if (!env) { + return undefined; + } -@injectable() -class CurrentPathServiceProxy extends BaseLocatorServiceProxy { - constructor( - @inject(IInterpreterHelper) helper: IInterpreterHelper, - @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, - @inject(IPythonInPathCommandProvider) pythonCommandProvider: IPythonInPathCommandProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - super(new CurrentPathService(helper, processServiceFactory, pythonCommandProvider, serviceContainer)); + return { name: env.name, path: env.location }; } -} -@injectable() -class GlobalVirtualEnvServiceProxy extends BaseLocatorServiceProxy { - public constructor( - @inject(IVirtualEnvironmentsSearchPathProvider) - @named('global') - globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - super(new GlobalVirtualEnvService(globalVirtualEnvPathProvider, serviceContainer)); + // eslint-disable-next-line class-methods-use-this + public async isMicrosoftStoreInterpreter(pythonPath: string): Promise { + // Eventually we won't be calling 'isMicrosoftStoreInterpreter' in the component adapter, so we won't + // need to use 'isMicrosoftStoreEnvironment' directly here. This is just a temporary implementation. + return isMicrosoftStoreEnvironment(pythonPath); } -} -@injectable() -class WorkspaceVirtualEnvServiceProxy extends BaseLocatorServiceProxy { - public constructor( - @inject(IVirtualEnvironmentsSearchPathProvider) - @named('workspace') - workspaceVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IInterpreterWatcherBuilder) builder: IInterpreterWatcherBuilder - ) { - super(new WorkspaceVirtualEnvService(workspaceVirtualEnvPathProvider, serviceContainer, builder)); + // Implements IInterpreterLocatorService + public async hasInterpreters( + filter: (e: PythonEnvironment) => Promise = async () => true, + ): Promise { + const onAddedToCollection = createDeferred(); + // Watch for collection changed events. + this.api.onChanged(async (e: PythonEnvCollectionChangedEvent) => { + if (e.new) { + if (await filter(convertEnvInfo(e.new))) { + onAddedToCollection.resolve(); + } + } + }); + const initialEnvs = await asyncFilter(this.api.getEnvs(), (e) => filter(convertEnvInfo(e))); + if (initialEnvs.length > 0) { + return true; + } + // Wait for an env to be added to the collection until the refresh has finished. Note although it's not + // guaranteed we have initiated discovery in this session, we do trigger refresh in the very first session, + // when Python is not installed, etc. Assuming list is more or less upto date. + await Promise.race([onAddedToCollection.promise, this.api.getRefreshPromise()]); + const envs = await asyncFilter(this.api.getEnvs(), (e) => filter(convertEnvInfo(e))); + return envs.length > 0; } -} -@injectable() -class KnownPathsServiceProxy extends BaseLocatorServiceProxy { - public constructor( - @inject(IKnownSearchPathsForInterpreters) knownSearchPaths: IKnownSearchPathsForInterpreters, - @inject(IInterpreterHelper) helper: IInterpreterHelper, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - super(new KnownPathsService(knownSearchPaths, helper, serviceContainer)); + public getInterpreters(resource?: vscode.Uri, source?: PythonEnvSource[]): PythonEnvironment[] { + const query: PythonLocatorQuery = {}; + let roots: vscode.Uri[] = []; + let wsFolder: vscode.WorkspaceFolder | undefined; + if (resource !== undefined) { + wsFolder = vscode.workspace.getWorkspaceFolder(resource); + if (wsFolder) { + roots = [wsFolder.uri]; + } + } + // Untitled files should still use the workspace as the query location + if ( + !wsFolder && + vscode.workspace.workspaceFolders && + vscode.workspace.workspaceFolders.length > 0 && + (!resource || resource.scheme === 'untitled') + ) { + roots = vscode.workspace.workspaceFolders.map((w) => w.uri); + } + + query.searchLocations = { + roots, + }; + + let envs = this.api.getEnvs(query); + if (source) { + envs = envs.filter((env) => intersection(source, env.source).length > 0); + } + + return envs.map(convertEnvInfo); } -} -@injectable() -class PipEnvLocatorServiceProxy extends BaseLocatorServiceProxy { - // This is only meant for consumption by PipEnvServiceProxy. - public readonly pipEnvService: IPipEnvService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(new PipEnvService(serviceContainer)); - this.pipEnvService = (this.impl as unknown) as IPipEnvService; + public async getWorkspaceVirtualEnvInterpreters( + resource: vscode.Uri, + options?: { ignoreCache?: boolean }, + ): Promise { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(resource); + if (!workspaceFolder) { + return []; + } + const query: PythonLocatorQuery = { + searchLocations: { + roots: [workspaceFolder.uri], + doNotIncludeNonRooted: true, + }, + }; + if (options?.ignoreCache) { + await this.api.triggerRefresh(query); + } + await this.api.getRefreshPromise(); + const envs = this.api.getEnvs(query); + return envs.map(convertEnvInfo); } } -@injectable() -class WindowsRegistryServiceProxy extends BaseLocatorServiceProxy { - constructor( - @inject(IRegistry) registry: IRegistry, - @inject(IPlatformService) platform: IPlatformService, - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IWindowsStoreInterpreter) windowsStoreInterpreter: IWindowsStoreInterpreter - ) { - super(new WindowsRegistryService(registry, platform, serviceContainer, windowsStoreInterpreter)); - } +export function registerNewDiscoveryForIOC(serviceManager: IServiceManager, api: IDiscoveryAPI): void { + serviceManager.addSingleton(ICondaService, CondaService); + serviceManager.addSingletonInstance(IComponentAdapter, new ComponentAdapter(api)); } diff --git a/src/client/pythonEnvironments/nativeAPI.ts b/src/client/pythonEnvironments/nativeAPI.ts new file mode 100644 index 000000000000..62695c8dd543 --- /dev/null +++ b/src/client/pythonEnvironments/nativeAPI.ts @@ -0,0 +1,548 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Event, EventEmitter, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType, PythonVersion } from './base/info'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from './base/locator'; +import { PythonEnvCollectionChangedEvent } from './base/watcher'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonFinder, +} from './base/locators/common/nativePythonFinder'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { Architecture, getPathEnvVariable, getUserHomeDir } from '../common/utils/platform'; +import { parseVersion } from './base/info/pythonVersion'; +import { cache } from '../common/utils/decorators'; +import { traceError, traceInfo, traceLog, traceWarn } from '../logging'; +import { StopWatch } from '../common/utils/stopWatch'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { categoryToKind, NativePythonEnvironmentKind } from './base/locators/common/nativePythonUtils'; +import { getCondaEnvDirs, getCondaPathSetting, setCondaBinary } from './common/environmentManagers/conda'; +import { setPyEnvBinary } from './common/environmentManagers/pyenv'; +import { + createPythonWatcher, + PythonGlobalEnvEvent, + PythonWorkspaceEnvEvent, +} from './base/locators/common/pythonWatcher'; +import { getWorkspaceFolders, onDidChangeWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +function makeExecutablePath(prefix?: string): string { + if (!prefix) { + return process.platform === 'win32' ? 'python.exe' : 'python'; + } + return process.platform === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'python'); +} + +function toArch(a: string | undefined): Architecture { + switch (a) { + case 'x86': + return Architecture.x86; + case 'x64': + return Architecture.x64; + default: + return Architecture.Unknown; + } +} + +function getLocation(nativeEnv: NativeEnvInfo, executable: string): string { + if (nativeEnv.kind === NativePythonEnvironmentKind.Conda) { + return nativeEnv.prefix ?? path.dirname(executable); + } + + if (nativeEnv.executable) { + return nativeEnv.executable; + } + + if (nativeEnv.prefix) { + return nativeEnv.prefix; + } + + // This is a path to a generated executable. Needed for backwards compatibility. + return executable; +} + +function kindToShortString(kind: PythonEnvKind): string | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + return 'poetry'; + case PythonEnvKind.Pyenv: + return 'pyenv'; + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + return 'venv'; + case PythonEnvKind.Pipenv: + return 'pipenv'; + case PythonEnvKind.Conda: + return 'conda'; + case PythonEnvKind.ActiveState: + return 'active-state'; + case PythonEnvKind.MicrosoftStore: + return 'Microsoft Store'; + case PythonEnvKind.Hatch: + return 'hatch'; + case PythonEnvKind.Pixi: + return 'pixi'; + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + default: + return undefined; + } +} + +function toShortVersionString(version: PythonVersion): string { + return `${version.major}.${version.minor}.${version.micro}`.trim(); +} + +function getDisplayName(version: PythonVersion, kind: PythonEnvKind, arch: Architecture, name?: string): string { + const versionStr = toShortVersionString(version); + const kindStr = kindToShortString(kind); + if (arch === Architecture.x86) { + if (kindStr) { + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit (${kindStr})`; + } + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit`; + } + if (kindStr) { + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr} (${kindStr})`; + } + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr}`; +} + +function validEnv(nativeEnv: NativeEnvInfo): boolean { + if (nativeEnv.prefix === undefined && nativeEnv.executable === undefined) { + traceError(`Invalid environment [native]: ${JSON.stringify(nativeEnv)}`); + return false; + } + return true; +} + +function getEnvType(kind: PythonEnvKind): PythonEnvType | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + case PythonEnvKind.Pyenv: + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + case PythonEnvKind.Pipenv: + case PythonEnvKind.ActiveState: + case PythonEnvKind.Hatch: + case PythonEnvKind.Pixi: + return PythonEnvType.Virtual; + + case PythonEnvKind.Conda: + return PythonEnvType.Conda; + + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + case PythonEnvKind.MicrosoftStore: + default: + return undefined; + } +} + +function isSubDir(pathToCheck: string | undefined, parents: string[]): boolean { + return parents.some((prefix) => { + if (pathToCheck) { + return path.normalize(pathToCheck).startsWith(path.normalize(prefix)); + } + return false; + }); +} + +function foundOnPath(fsPath: string): boolean { + const paths = getPathEnvVariable().map((p) => path.normalize(p).toLowerCase()); + const normalized = path.normalize(fsPath).toLowerCase(); + return paths.some((p) => normalized.includes(p)); +} + +function getName(nativeEnv: NativeEnvInfo, kind: PythonEnvKind, condaEnvDirs: string[]): string { + if (nativeEnv.name) { + return nativeEnv.name; + } + + const envType = getEnvType(kind); + if (nativeEnv.prefix && envType === PythonEnvType.Virtual) { + return path.basename(nativeEnv.prefix); + } + + if (nativeEnv.prefix && envType === PythonEnvType.Conda) { + if (nativeEnv.name === 'base') { + return 'base'; + } + + const workspaces = (getWorkspaceFolders() ?? []).map((wf) => wf.uri.fsPath); + if (isSubDir(nativeEnv.prefix, workspaces)) { + traceInfo(`Conda env is --prefix environment: ${nativeEnv.prefix}`); + return ''; + } + + if (condaEnvDirs.length > 0 && isSubDir(nativeEnv.prefix, condaEnvDirs)) { + traceInfo(`Conda env is --named environment: ${nativeEnv.prefix}`); + return path.basename(nativeEnv.prefix); + } + } + + return ''; +} + +function toPythonEnvInfo(nativeEnv: NativeEnvInfo, condaEnvDirs: string[]): PythonEnvInfo | undefined { + if (!validEnv(nativeEnv)) { + return undefined; + } + const kind = categoryToKind(nativeEnv.kind); + const arch = toArch(nativeEnv.arch); + const version: PythonVersion = parseVersion(nativeEnv.version ?? ''); + const name = getName(nativeEnv, kind, condaEnvDirs); + const displayName = nativeEnv.version + ? getDisplayName(version, kind, arch, name) + : nativeEnv.displayName ?? 'Python'; + + const executable = nativeEnv.executable ?? makeExecutablePath(nativeEnv.prefix); + return { + name, + location: getLocation(nativeEnv, executable), + kind, + id: executable, + executable: { + filename: executable, + sysPrefix: nativeEnv.prefix ?? '', + ctime: -1, + mtime: -1, + }, + version: { + sysVersion: nativeEnv.version, + major: version.major, + minor: version.minor, + micro: version.micro, + }, + arch, + distro: { + org: '', + }, + source: [], + detailedDisplayName: displayName, + display: displayName, + type: getEnvType(kind), + }; +} + +function hasChanged(old: PythonEnvInfo, newEnv: PythonEnvInfo): boolean { + if (old.name !== newEnv.name) { + return true; + } + if (old.executable.filename !== newEnv.executable.filename) { + return true; + } + if (old.version.major !== newEnv.version.major) { + return true; + } + if (old.version.minor !== newEnv.version.minor) { + return true; + } + if (old.version.micro !== newEnv.version.micro) { + return true; + } + if (old.location !== newEnv.location) { + return true; + } + if (old.kind !== newEnv.kind) { + return true; + } + if (old.arch !== newEnv.arch) { + return true; + } + + return false; +} + +class NativePythonEnvironments implements IDiscoveryAPI, Disposable { + private _onProgress: EventEmitter; + + private _onChanged: EventEmitter; + + private _refreshPromise?: Deferred; + + private _envs: PythonEnvInfo[] = []; + + private _disposables: Disposable[] = []; + + private _condaEnvDirs: string[] = []; + + constructor(private readonly finder: NativePythonFinder) { + this._onProgress = new EventEmitter(); + this._onChanged = new EventEmitter(); + + this.onProgress = this._onProgress.event; + this.onChanged = this._onChanged.event; + + this.refreshState = ProgressReportStage.idle; + this._disposables.push(this._onProgress, this._onChanged); + + this.initializeWatcher(); + } + + dispose(): void { + this._disposables.forEach((d) => d.dispose()); + } + + refreshState: ProgressReportStage; + + onProgress: Event; + + onChanged: Event; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise { + const stopwatch = new StopWatch(); + traceLog('Native locator: Refresh started'); + if (this.refreshState === ProgressReportStage.discoveryStarted && this._refreshPromise?.promise) { + return this._refreshPromise?.promise; + } + + this.refreshState = ProgressReportStage.discoveryStarted; + this._onProgress.fire({ stage: this.refreshState }); + this._refreshPromise = createDeferred(); + + setImmediate(async () => { + try { + const before = this._envs.map((env) => env.executable.filename); + const after: string[] = []; + for await (const native of this.finder.refresh()) { + const exe = this.processNative(native); + if (exe) { + after.push(exe); + } + } + const envsToRemove = before.filter((item) => !after.includes(item)); + envsToRemove.forEach((item) => this.removeEnv(item)); + this._refreshPromise?.resolve(); + } catch (error) { + this._refreshPromise?.reject(error); + } finally { + traceLog(`Native locator: Refresh finished in ${stopwatch.elapsedTime} ms`); + this.refreshState = ProgressReportStage.discoveryFinished; + this._refreshPromise = undefined; + this._onProgress.fire({ stage: this.refreshState }); + } + }); + + return this._refreshPromise?.promise; + } + + private processNative(native: NativeEnvInfo | NativeEnvManagerInfo): string | undefined { + if (isNativeEnvInfo(native)) { + return this.processEnv(native); + } + this.processEnvManager(native); + + return undefined; + } + + private processEnv(native: NativeEnvInfo): string | undefined { + if (!validEnv(native)) { + return undefined; + } + + try { + const version = native.version ? parseVersion(native.version) : undefined; + + if (categoryToKind(native.kind) === PythonEnvKind.Conda && !native.executable) { + // This is a conda env without python, no point trying to resolve this. + // There is nothing to resolve + return this.addEnv(native)?.executable.filename; + } + if (native.executable && (!version || version.major < 0 || version.minor < 0 || version.micro < 0)) { + // We have a path, but no version info, try to resolve the environment. + this.finder + .resolve(native.executable) + .then((env) => { + if (env) { + this.addEnv(env); + } + }) + .ignoreErrors(); + return native.executable; + } + if (native.executable && version && version.major >= 0 && version.minor >= 0 && version.micro >= 0) { + return this.addEnv(native)?.executable.filename; + } + traceError(`Failed to process environment: ${JSON.stringify(native)}`); + } catch (err) { + traceError(`Failed to process environment: ${err}`); + } + return undefined; + } + + private condaPathAlreadySet: string | undefined; + + // eslint-disable-next-line class-methods-use-this + private processEnvManager(native: NativeEnvManagerInfo) { + const tool = native.tool.toLowerCase(); + switch (tool) { + case 'conda': + { + traceLog(`Conda environment manager found at: ${native.executable}`); + const settingPath = getCondaPathSetting(); + if (!this.condaPathAlreadySet) { + if (settingPath === '' || settingPath === undefined) { + if (foundOnPath(native.executable)) { + setCondaBinary(native.executable); + this.condaPathAlreadySet = native.executable; + traceInfo(`Using conda: ${native.executable}`); + } else { + traceInfo(`Conda not found on PATH, skipping: ${native.executable}`); + traceInfo( + 'You can set the path to conda using the setting: `python.condaPath` if you want to use a different conda binary', + ); + } + } else { + traceInfo(`Using conda from setting: ${settingPath}`); + this.condaPathAlreadySet = settingPath; + } + } else { + traceInfo(`Conda set to: ${this.condaPathAlreadySet}`); + } + } + break; + case 'pyenv': + traceLog(`Pyenv environment manager found at: ${native.executable}`); + setPyEnvBinary(native.executable); + break; + case 'poetry': + traceLog(`Poetry environment manager found at: ${native.executable}`); + break; + default: + traceWarn(`Unknown environment manager: ${native.tool}`); + break; + } + } + + getEnvs(_query?: PythonLocatorQuery): PythonEnvInfo[] { + return this._envs; + } + + private addEnv(native: NativeEnvInfo, searchLocation?: Uri): PythonEnvInfo | undefined { + const info = toPythonEnvInfo(native, this._condaEnvDirs); + if (info) { + const old = this._envs.find((item) => item.executable.filename === info.executable.filename); + if (old) { + this._envs = this._envs.filter((item) => item.executable.filename !== info.executable.filename); + this._envs.push(info); + if (hasChanged(old, info)) { + this._onChanged.fire({ type: FileChangeType.Changed, old, new: info, searchLocation }); + } + } else { + this._envs.push(info); + this._onChanged.fire({ type: FileChangeType.Created, new: info, searchLocation }); + } + } + + return info; + } + + private removeEnv(env: PythonEnvInfo | string): void { + if (typeof env === 'string') { + const old = this._envs.find((item) => item.executable.filename === env); + this._envs = this._envs.filter((item) => item.executable.filename !== env); + this._onChanged.fire({ type: FileChangeType.Deleted, old }); + return; + } + this._envs = this._envs.filter((item) => item.executable.filename !== env.executable.filename); + this._onChanged.fire({ type: FileChangeType.Deleted, old: env }); + } + + @cache(30_000, true) + async resolveEnv(envPath?: string): Promise { + if (envPath === undefined) { + return undefined; + } + try { + const native = await this.finder.resolve(envPath); + if (native) { + if (native.kind === NativePythonEnvironmentKind.Conda && this._condaEnvDirs.length === 0) { + this._condaEnvDirs = (await getCondaEnvDirs()) ?? []; + } + return this.addEnv(native); + } + return undefined; + } catch { + return undefined; + } + } + + private initializeWatcher(): void { + const watcher = createPythonWatcher(); + this._disposables.push( + watcher.onDidGlobalEnvChanged((e) => this.pathEventHandler(e)), + watcher.onDidWorkspaceEnvChanged(async (e) => { + await this.workspaceEventHandler(e); + }), + onDidChangeWorkspaceFolders((e: WorkspaceFoldersChangeEvent) => { + e.removed.forEach((wf) => watcher.unwatchWorkspace(wf)); + e.added.forEach((wf) => watcher.watchWorkspace(wf)); + }), + watcher, + ); + + getWorkspaceFolders()?.forEach((wf) => watcher.watchWorkspace(wf)); + const home = getUserHomeDir(); + if (home) { + watcher.watchPath(Uri.file(path.join(home, '.conda', 'environments.txt'))); + } + } + + private async pathEventHandler(e: PythonGlobalEnvEvent): Promise { + if (e.type === FileChangeType.Created || e.type === FileChangeType.Changed) { + if (e.uri.fsPath.endsWith('environment.txt')) { + const before = this._envs + .filter((env) => env.kind === PythonEnvKind.Conda) + .map((env) => env.executable.filename); + for await (const native of this.finder.refresh(NativePythonEnvironmentKind.Conda)) { + this.processNative(native); + } + const after = this._envs + .filter((env) => env.kind === PythonEnvKind.Conda) + .map((env) => env.executable.filename); + const envsToRemove = before.filter((item) => !after.includes(item)); + envsToRemove.forEach((item) => this.removeEnv(item)); + } + } + } + + private async workspaceEventHandler(e: PythonWorkspaceEnvEvent): Promise { + if (e.type === FileChangeType.Created || e.type === FileChangeType.Changed) { + const native = await this.finder.resolve(e.executable); + if (native) { + this.addEnv(native, e.workspaceFolder.uri); + } + } else { + this.removeEnv(e.executable); + } + } +} + +export function createNativeEnvironmentsApi(finder: NativePythonFinder): IDiscoveryAPI & Disposable { + const native = new NativePythonEnvironments(finder); + native.triggerRefresh().ignoreErrors(); + return native; +} diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts deleted file mode 100644 index 94bc5cba1b7a..000000000000 --- a/src/client/refactor/proxy.ts +++ /dev/null @@ -1,209 +0,0 @@ -// 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/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts new file mode 100644 index 000000000000..3f8a085da467 --- /dev/null +++ b/src/client/repl/nativeRepl.ts @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Native Repl class that holds instance of pythonServer and replController + +import { NotebookController, NotebookDocument, QuickPickItem, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import * as path from 'path'; +import { Disposable } from 'vscode-jsonrpc'; +import { PVSC_EXTENSION_ID } from '../common/constants'; +import { showNotebookDocument, showQuickPick } from '../common/vscodeApis/windowApis'; +import { getWorkspaceFolders, onDidCloseNotebookDocument } from '../common/vscodeApis/workspaceApis'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { createPythonServer, PythonServer } from './pythonServer'; +import { executeNotebookCell, openInteractiveREPL, selectNotebookKernel } from './replCommandHandler'; +import { createReplController } from './replController'; +import { EventName } from '../telemetry/constants'; +import { sendTelemetryEvent } from '../telemetry'; +import { VariablesProvider } from './variables/variablesProvider'; +import { VariableRequester } from './variables/variableRequester'; +import { getTabNameForUri } from './replUtils'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../common/persistentState'; +import { onDidChangeEnvironmentEnvExt, useEnvExtension } from '../envExt/api.internal'; +import { getActiveInterpreterLegacy } from '../envExt/api.legacy'; + +export const NATIVE_REPL_URI_MEMENTO = 'nativeReplUri'; +let nativeRepl: NativeRepl | undefined; +export class NativeRepl implements Disposable { + // Adding ! since it will get initialized in create method, not the constructor. + private pythonServer!: PythonServer; + + private cwd: string | undefined; + + private interpreter!: PythonEnvironment; + + private disposables: Disposable[] = []; + + private replController!: NotebookController; + + private notebookDocument: NotebookDocument | undefined; + + public newReplSession: boolean | undefined = true; + + private envChangeListenerRegistered = false; + + private pendingInterpreterChange?: { resource?: Uri }; + + // TODO: In the future, could also have attribute of URI for file specific REPL. + private constructor() { + this.watchNotebookClosed(); + } + + // Static async factory method to handle asynchronous initialization + public static async create(interpreter: PythonEnvironment): Promise { + const nativeRepl = new NativeRepl(); + nativeRepl.interpreter = interpreter; + await nativeRepl.setReplDirectory(); + nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd); + nativeRepl.disposables.push(nativeRepl.pythonServer); + nativeRepl.setReplController(); + nativeRepl.registerInterpreterChangeHandler(); + + return nativeRepl; + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } + + /** + * Function that watches for Notebook Closed event. + * This is for the purposes of correctly updating the notebookEditor and notebookDocument on close. + */ + private watchNotebookClosed(): void { + this.disposables.push( + onDidCloseNotebookDocument(async (nb) => { + if (this.notebookDocument && nb.uri.toString() === this.notebookDocument.uri.toString()) { + this.notebookDocument = undefined; + this.newReplSession = true; + await updateWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO, undefined); + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([this.interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + nativeRepl = undefined; + } + }), + ); + } + + /** + * Function that set up desired directory for REPL. + * If there is multiple workspaces, prompt the user to choose + * which directory we should set in context of native REPL. + */ + private async setReplDirectory(): Promise { + // Figure out uri via workspaceFolder as uri parameter always + // seem to be undefined from parameter when trying to access from replCommands.ts + const workspaces: readonly WorkspaceFolder[] | undefined = getWorkspaceFolders(); + + if (workspaces) { + // eslint-disable-next-line no-shadow + const workspacesQuickPickItems: QuickPickItem[] = workspaces.map((workspace) => ({ + label: workspace.name, + description: workspace.uri.fsPath, + })); + + if (workspacesQuickPickItems.length === 0) { + this.cwd = process.cwd(); // Yields '/' on no workspace scenario. + } else if (workspacesQuickPickItems.length === 1) { + this.cwd = workspacesQuickPickItems[0].description; + } else { + // Show choices of workspaces for user to choose from. + const selection = (await showQuickPick(workspacesQuickPickItems, { + placeHolder: 'Select current working directory for new REPL', + matchOnDescription: true, + ignoreFocusOut: true, + })) as QuickPickItem; + this.cwd = selection?.description; + } + } + } + + /** + * Function that check if NotebookController for REPL exists, and returns it in Singleton manner. + */ + public setReplController(force: boolean = false): NotebookController { + if (!this.replController || force) { + this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd); + this.replController.variableProvider = new VariablesProvider( + new VariableRequester(this.pythonServer), + () => this.notebookDocument, + this.pythonServer.onCodeExecuted, + ); + } + return this.replController; + } + + private registerInterpreterChangeHandler(): void { + if (!useEnvExtension() || this.envChangeListenerRegistered) { + return; + } + this.envChangeListenerRegistered = true; + this.disposables.push( + onDidChangeEnvironmentEnvExt((event) => { + this.updateInterpreterForChange(event.uri).catch(() => undefined); + }), + ); + this.disposables.push( + this.pythonServer.onCodeExecuted(() => { + if (this.pendingInterpreterChange) { + const { resource } = this.pendingInterpreterChange; + this.pendingInterpreterChange = undefined; + this.updateInterpreterForChange(resource).catch(() => undefined); + } + }), + ); + } + + private async updateInterpreterForChange(resource?: Uri): Promise { + if (this.pythonServer?.isExecuting) { + this.pendingInterpreterChange = { resource }; + return; + } + if (!this.shouldApplyInterpreterChange(resource)) { + return; + } + const scope = resource ?? (this.cwd ? Uri.file(this.cwd) : undefined); + const interpreter = await getActiveInterpreterLegacy(scope); + if (!interpreter || interpreter.path === this.interpreter?.path) { + return; + } + + this.interpreter = interpreter; + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + this.setReplController(true); + + if (this.notebookDocument) { + const notebookEditor = await showNotebookDocument(this.notebookDocument, { preserveFocus: true }); + await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + } + + private shouldApplyInterpreterChange(resource?: Uri): boolean { + if (!resource || !this.cwd) { + return true; + } + const relative = path.relative(this.cwd, resource.fsPath); + return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); + } + + /** + * Function that checks if native REPL's text input box contains complete code. + * @returns Promise - True if complete/Valid code is present, False otherwise. + */ + public async checkUserInputCompleteCode(activeEditor: TextEditor | undefined): Promise { + let completeCode = false; + let userTextInput; + if (activeEditor) { + const { document } = activeEditor; + userTextInput = document.getText(); + } + + // Check if userTextInput is a complete Python command + if (userTextInput) { + completeCode = await this.pythonServer.checkValidCommand(userTextInput); + } + + return completeCode; + } + + /** + * Function that opens interactive repl, selects kernel, and send/execute code to the native repl. + */ + public async sendToNativeRepl(code?: string | undefined, preserveFocus: boolean = true): Promise { + let wsMementoUri: Uri | undefined; + + if (!this.notebookDocument) { + const wsMemento = getWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO); + wsMementoUri = wsMemento ? Uri.parse(wsMemento) : undefined; + + if (!wsMementoUri || getTabNameForUri(wsMementoUri) !== 'Python REPL') { + await updateWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO, undefined); + wsMementoUri = undefined; + } + } + + const result = await openInteractiveREPL(this.notebookDocument ?? wsMementoUri, preserveFocus); + if (result) { + this.notebookDocument = result.notebookEditor.notebook; + await updateWorkspaceStateValue( + NATIVE_REPL_URI_MEMENTO, + this.notebookDocument.uri.toString(), + ); + + if (result.documentCreated) { + await selectNotebookKernel(result.notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + if (code) { + await executeNotebookCell(result.notebookEditor, code); + } + } + } +} + +/** + * Get Singleton Native REPL Instance + * @param interpreter + * @returns Native REPL instance + */ +export async function getNativeRepl(interpreter: PythonEnvironment, disposables: Disposable[]): Promise { + if (!nativeRepl) { + nativeRepl = await NativeRepl.create(interpreter); + disposables.push(nativeRepl); + } + if (nativeRepl && nativeRepl.newReplSession) { + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' }); + nativeRepl.newReplSession = false; + } + return nativeRepl; +} diff --git a/src/client/repl/pythonServer.ts b/src/client/repl/pythonServer.ts new file mode 100644 index 000000000000..c4b1722b5079 --- /dev/null +++ b/src/client/repl/pythonServer.ts @@ -0,0 +1,168 @@ +import * as path from 'path'; +import * as ch from 'child_process'; +import * as rpc from 'vscode-jsonrpc/node'; +import { Disposable, Event, EventEmitter, window } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { traceError, traceLog } from '../logging'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +const SERVER_PATH = path.join(EXTENSION_ROOT_DIR, 'python_files', 'python_server.py'); +let serverInstance: PythonServer | undefined; +export interface ExecutionResult { + status: boolean; + output: string; +} + +export interface PythonServer extends Disposable { + onCodeExecuted: Event; + readonly isExecuting: boolean; + readonly isDisposed: boolean; + execute(code: string): Promise; + executeSilently(code: string): Promise; + interrupt(): void; + input(): void; + checkValidCommand(code: string): Promise; +} + +class PythonServerImpl implements PythonServer, Disposable { + private readonly disposables: Disposable[] = []; + + private readonly _onCodeExecuted = new EventEmitter(); + + onCodeExecuted = this._onCodeExecuted.event; + + private inFlightRequests = 0; + + private disposed = false; + + public get isExecuting(): boolean { + return this.inFlightRequests > 0; + } + + public get isDisposed(): boolean { + return this.disposed; + } + + constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) { + this.initialize(); + this.input(); + } + + private initialize(): void { + this.disposables.push( + this.connection.onNotification('log', (message: string) => { + traceLog('Log:', message); + }), + ); + this.pythonServer.on('exit', (code) => { + traceError(`Python server exited with code ${code}`); + this.markDisposed(); + }); + this.pythonServer.on('error', (err) => { + traceError(err); + this.markDisposed(); + }); + this.connection.listen(); + } + + public input(): void { + // Register input request handler + this.connection.onRequest('input', async (request) => { + // Ask for user input via popup quick input, send it back to Python + let userPrompt = 'Enter your input here: '; + if (request && request.prompt) { + userPrompt = request.prompt; + } + const input = await window.showInputBox({ + title: 'Input Request', + prompt: userPrompt, + ignoreFocusOut: true, + }); + return { userInput: input }; + }); + } + + @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) + public async execute(code: string): Promise { + const result = await this.executeCode(code); + if (result?.status) { + this._onCodeExecuted.fire(); + } + return result; + } + + public executeSilently(code: string): Promise { + return this.executeCode(code); + } + + private async executeCode(code: string): Promise { + this.inFlightRequests += 1; + try { + const result = await this.connection.sendRequest('execute', code); + return result as ExecutionResult; + } catch (err) { + const error = err as Error; + traceError(`Error getting response from REPL server:`, error); + } finally { + this.inFlightRequests -= 1; + } + return undefined; + } + + public interrupt(): void { + // Passing SIGINT to interrupt only would work for Mac and Linux + if (this.pythonServer.kill('SIGINT')) { + traceLog('Python REPL server interrupted'); + } + } + + public async checkValidCommand(code: string): Promise { + this.inFlightRequests += 1; + try { + const completeCode: ExecutionResult = await this.connection.sendRequest('check_valid_command', code); + return completeCode.output === 'True'; + } finally { + this.inFlightRequests -= 1; + } + } + + public dispose(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.connection.sendNotification('exit'); + this.disposables.forEach((d) => d.dispose()); + this.connection.dispose(); + serverInstance = undefined; + } + + private markDisposed(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.connection.dispose(); + serverInstance = undefined; + } +} + +export function createPythonServer(interpreter: string[], cwd?: string): PythonServer { + if (serverInstance && !serverInstance.isDisposed) { + return serverInstance; + } + + const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH], { + cwd, // Launch with correct workspace directory + }); + pythonServer.stderr.on('data', (data) => { + traceError(data.toString()); + }); + const connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(pythonServer.stdout), + new rpc.StreamMessageWriter(pythonServer.stdin), + ); + serverInstance = new PythonServerImpl(connection, pythonServer); + return serverInstance; +} diff --git a/src/client/repl/replCommandHandler.ts b/src/client/repl/replCommandHandler.ts new file mode 100644 index 000000000000..630eddfdd565 --- /dev/null +++ b/src/client/repl/replCommandHandler.ts @@ -0,0 +1,98 @@ +import { + NotebookEditor, + ViewColumn, + NotebookDocument, + NotebookCellData, + NotebookCellKind, + NotebookEdit, + WorkspaceEdit, + Uri, +} from 'vscode'; +import { getExistingReplViewColumn, getTabNameForUri } from './replUtils'; +import { showNotebookDocument } from '../common/vscodeApis/windowApis'; +import { openNotebookDocument, applyEdit } from '../common/vscodeApis/workspaceApis'; +import { executeCommand } from '../common/vscodeApis/commandApis'; + +/** + * Function that opens/show REPL using IW UI. + */ +export async function openInteractiveREPL( + notebookDocument: NotebookDocument | Uri | undefined, + preserveFocus: boolean = true, +): Promise<{ notebookEditor: NotebookEditor; documentCreated: boolean } | undefined> { + let viewColumn = ViewColumn.Beside; + let alreadyExists = false; + if (notebookDocument instanceof Uri) { + // Case where NotebookDocument is undefined, but workspace mementoURI exists. + notebookDocument = await openNotebookDocument(notebookDocument); + } else if (notebookDocument) { + // Case where NotebookDocument (REPL document already exists in the tab) + const existingReplViewColumn = getExistingReplViewColumn(notebookDocument); + viewColumn = existingReplViewColumn ?? viewColumn; + alreadyExists = true; + } else if (!notebookDocument) { + // Case where NotebookDocument doesnt exist, or + // became outdated (untitled.ipynb created without Python extension knowing, effectively taking over original Python REPL's URI) + notebookDocument = await openNotebookDocument('jupyter-notebook'); + } + + const notebookEditor = await showNotebookDocument(notebookDocument!, { + viewColumn, + asRepl: 'Python REPL', + preserveFocus, + }); + + // Sanity check that we opened a Native REPL from showNotebookDocument. + if ( + !notebookEditor || + !notebookEditor.notebook || + !notebookEditor.notebook.uri || + getTabNameForUri(notebookEditor.notebook.uri) !== 'Python REPL' + ) { + return undefined; + } + + return { notebookEditor, documentCreated: !alreadyExists }; +} + +/** + * Function that selects notebook Kernel. + */ +export async function selectNotebookKernel( + notebookEditor: NotebookEditor, + notebookControllerId: string, + extensionId: string, +): Promise { + await executeCommand('notebook.selectKernel', { + notebookEditor, + id: notebookControllerId, + extension: extensionId, + }); +} + +/** + * Function that executes notebook cell given code. + */ +export async function executeNotebookCell(notebookEditor: NotebookEditor, code: string): Promise { + const { notebook, replOptions } = notebookEditor; + const cellIndex = replOptions?.appendIndex ?? notebook.cellCount; + await addCellToNotebook(notebook, cellIndex, code); + // Execute the cell + executeCommand('notebook.cell.execute', { + ranges: [{ start: cellIndex, end: cellIndex + 1 }], + document: notebook.uri, + }); +} + +/** + * Function that adds cell to notebook. + * This function will only get called when notebook document is defined. + */ +async function addCellToNotebook(notebookDocument: NotebookDocument, index: number, code: string): Promise { + const notebookCellData = new NotebookCellData(NotebookCellKind.Code, code as string, 'python'); + // Add new cell to interactive window document + const notebookEdit = NotebookEdit.insertCells(index, [notebookCellData]); + const workspaceEdit = new WorkspaceEdit(); + workspaceEdit.set(notebookDocument!.uri, [notebookEdit]); + await applyEdit(workspaceEdit); +} diff --git a/src/client/repl/replCommands.ts b/src/client/repl/replCommands.ts new file mode 100644 index 000000000000..1171e9466ee8 --- /dev/null +++ b/src/client/repl/replCommands.ts @@ -0,0 +1,131 @@ +import { commands, Uri, window } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { ICommandManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { IInterpreterService } from '../interpreter/contracts'; +import { ICodeExecutionHelper } from '../terminals/types'; +import { getNativeRepl } from './nativeRepl'; +import { + executeInTerminal, + getActiveInterpreter, + getSelectedTextToExecute, + getSendToNativeREPLSetting, + insertNewLineToREPLInput, + isMultiLineText, +} from './replUtils'; +import { registerCommand } from '../common/vscodeApis/commandApis'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ReplType } from './types'; + +/** + * Register Start Native REPL command in the command palette + */ +export async function registerStartNativeReplCommand( + disposables: Disposable[], + interpreterService: IInterpreterService, +): Promise { + disposables.push( + registerCommand(Commands.Start_Native_REPL, async (uri: Uri) => { + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' }); + const interpreter = await getActiveInterpreter(uri, interpreterService); + if (interpreter) { + const nativeRepl = await getNativeRepl(interpreter, disposables); + await nativeRepl.sendToNativeRepl(undefined, false); + } + }), + ); +} + +/** + * Registers REPL command for shift+enter if sendToNativeREPL setting is enabled. + */ +export async function registerReplCommands( + disposables: Disposable[], + interpreterService: IInterpreterService, + executionHelper: ICodeExecutionHelper, + commandManager: ICommandManager, +): Promise { + disposables.push( + commandManager.registerCommand(Commands.Exec_In_REPL, async (uri: Uri) => { + const nativeREPLSetting = getSendToNativeREPLSetting(); + + if (!nativeREPLSetting) { + await executeInTerminal(); + return; + } + const interpreter = await getActiveInterpreter(uri, interpreterService); + + if (interpreter) { + const nativeRepl = await getNativeRepl(interpreter, disposables); + const activeEditor = window.activeTextEditor; + if (activeEditor) { + const code = await getSelectedTextToExecute(activeEditor); + if (code) { + // Smart Send + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await executionHelper.normalizeLines( + code!, + ReplType.native, + wholeFileContent, + ); + await nativeRepl.sendToNativeRepl(normalizedCode); + } + } + } + }), + ); +} + +/** + * Command triggered for 'Enter': Conditionally call interactive.execute OR insert \n in text input box. + */ +export async function registerReplExecuteOnEnter( + disposables: Disposable[], + interpreterService: IInterpreterService, + commandManager: ICommandManager, +): Promise { + disposables.push( + commandManager.registerCommand(Commands.Exec_In_REPL_Enter, async (uri: Uri) => { + await onInputEnter(uri, 'repl.execute', interpreterService, disposables); + }), + ); + disposables.push( + commandManager.registerCommand(Commands.Exec_In_IW_Enter, async (uri: Uri) => { + await onInputEnter(uri, 'interactive.execute', interpreterService, disposables); + }), + ); +} + +async function onInputEnter( + uri: Uri | undefined, + commandName: string, + interpreterService: IInterpreterService, + disposables: Disposable[], +): Promise { + const interpreter = await getActiveInterpreter(uri, interpreterService); + if (!interpreter) { + return; + } + + const nativeRepl = await getNativeRepl(interpreter, disposables); + const completeCode = await nativeRepl?.checkUserInputCompleteCode(window.activeTextEditor); + const editor = window.activeTextEditor; + + if (editor) { + // Execute right away when complete code and Not multi-line + if (completeCode && !isMultiLineText(editor)) { + await commands.executeCommand(commandName); + } else { + insertNewLineToREPLInput(editor); + + // Handle case when user enters on blank line, just trigger interactive.execute + if (editor && editor.document.lineAt(editor.selection.active.line).text === '') { + await commands.executeCommand(commandName); + } + } + } +} diff --git a/src/client/repl/replController.ts b/src/client/repl/replController.ts new file mode 100644 index 000000000000..f30b8d9cbf6f --- /dev/null +++ b/src/client/repl/replController.ts @@ -0,0 +1,40 @@ +import * as vscode from 'vscode'; +import { createPythonServer } from './pythonServer'; + +export function createReplController( + interpreterPath: string, + disposables: vscode.Disposable[], + cwd?: string, +): vscode.NotebookController { + const server = createPythonServer([interpreterPath], cwd); + disposables.push(server); + + const controller = vscode.notebooks.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL'); + controller.supportedLanguages = ['python']; + + controller.description = 'Python REPL'; + + controller.interruptHandler = async () => { + server.interrupt(); + }; + + controller.executeHandler = async (cells) => { + for (const cell of cells) { + const exec = controller.createNotebookCellExecution(cell); + exec.start(Date.now()); + + const result = await server.execute(cell.document.getText()); + + if (result?.output) { + exec.replaceOutput([ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(result.output, 'text/plain')]), + ]); + // TODO: Properly update via NotebookCellOutputItem.error later. + } + + exec.end(result?.status); + } + }; + disposables.push(controller); + return controller; +} diff --git a/src/client/repl/replUtils.ts b/src/client/repl/replUtils.ts new file mode 100644 index 000000000000..93ae6f2a4573 --- /dev/null +++ b/src/client/repl/replUtils.ts @@ -0,0 +1,135 @@ +import { NotebookDocument, TextEditor, Selection, Uri, commands, window, TabInputNotebook, ViewColumn } from 'vscode'; +import { Commands } from '../common/constants'; +import { noop } from '../common/utils/misc'; +import { getActiveResource } from '../common/vscodeApis/windowApis'; +import { getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { IInterpreterService } from '../interpreter/contracts'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { getMultiLineSelectionText, getSingleLineSelectionText } from '../terminals/codeExecution/helper'; + +/** + * Function that executes selected code in the terminal. + */ +export async function executeInTerminal(): Promise { + await commands.executeCommand(Commands.Exec_Selection_In_Terminal); +} + +/** + * Function that returns selected text to execute in the REPL. + * @param textEditor + * @returns code - Code to execute in the REPL. + */ +export async function getSelectedTextToExecute(textEditor: TextEditor): Promise { + const { selection } = textEditor; + let code: string; + + if (selection.isEmpty) { + code = textEditor.document.lineAt(selection.start.line).text; + } else if (selection.isSingleLine) { + code = getSingleLineSelectionText(textEditor); + } else { + code = getMultiLineSelectionText(textEditor); + } + + return code; +} + +/** + * Function that returns user's Native REPL setting. + * @returns boolean - True if sendToNativeREPL setting is enabled, False otherwise. + */ +export function getSendToNativeREPLSetting(): boolean { + const uri = getActiveResource(); + const configuration = getConfiguration('python', uri); + return configuration.get('REPL.sendToNativeREPL', false); +} + +// Function that inserts new line in the given (input) text editor +export function insertNewLineToREPLInput(activeEditor: TextEditor | undefined): void { + if (activeEditor) { + const position = activeEditor.selection.active; + const newPosition = position.with(position.line, activeEditor.document.lineAt(position.line).text.length); + activeEditor.selection = new Selection(newPosition, newPosition); + + activeEditor.edit((editBuilder) => { + editBuilder.insert(newPosition, '\n'); + }); + } +} + +export function isMultiLineText(textEditor: TextEditor): boolean { + return (textEditor?.document?.lineCount ?? 0) > 1; +} + +/** + * Function that trigger interpreter warning if invalid interpreter. + * Function will also return undefined or active interpreter + */ +export async function getActiveInterpreter( + uri: Uri | undefined, + interpreterService: IInterpreterService, +): Promise { + const resource = uri ?? getActiveResource(); + const interpreter = await interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + commands.executeCommand(Commands.TriggerEnvironmentSelection, resource).then(noop, noop); + return undefined; + } + return interpreter; +} + +/** + * Function that will return ViewColumn for existing Native REPL that belongs to given NotebookDocument. + */ +export function getExistingReplViewColumn(notebookDocument: NotebookDocument): ViewColumn | undefined { + const ourNotebookUri = notebookDocument.uri.toString(); + // Use Tab groups, to locate previously opened Python REPL tab and fetch view column. + const ourTb = window.tabGroups; + for (const tabGroup of ourTb.all) { + for (const tab of tabGroup.tabs) { + if (tab.label === 'Python REPL') { + const tabInput = (tab.input as unknown) as TabInputNotebook; + const tabUri = tabInput.uri.toString(); + if (tab.input && tabUri === ourNotebookUri) { + // This is the tab we are looking for. + const existingReplViewColumn = tab.group.viewColumn; + return existingReplViewColumn; + } + } + } + } + return undefined; +} + +/** + * Function that will return tab name for before reloading VS Code + * This is so we can make sure tab name is still 'Python REPL' after reloading VS Code, + * and make sure Python REPL does not get 'merged' into unaware untitled.ipynb tab. + */ +export function getTabNameForUri(uri: Uri): string | undefined { + const tabGroups = window.tabGroups.all; + + for (const tabGroup of tabGroups) { + for (const tab of tabGroup.tabs) { + if (tab.input instanceof TabInputNotebook && tab.input.uri.toString() === uri.toString()) { + return tab.label; + } + } + } + + return undefined; +} + +/** + * Function that will return the minor version of current active Python interpreter. + */ +export async function getPythonMinorVersion( + uri: Uri | undefined, + interpreterService: IInterpreterService, +): Promise { + if (uri) { + const pythonVersion = await getActiveInterpreter(uri, interpreterService); + return pythonVersion?.version?.minor; + } + return undefined; +} diff --git a/src/client/repl/types.ts b/src/client/repl/types.ts new file mode 100644 index 000000000000..38de9bfe2137 --- /dev/null +++ b/src/client/repl/types.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export enum ReplType { + terminal = 'terminal', + native = 'native', +} diff --git a/src/client/repl/variables/types.ts b/src/client/repl/variables/types.ts new file mode 100644 index 000000000000..1e3c80d32077 --- /dev/null +++ b/src/client/repl/variables/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CancellationToken, Variable } from 'vscode'; + +export interface IVariableDescription extends Variable { + /** The name of the variable at the root scope */ + root: string; + /** How to look up the specific property of the root variable */ + propertyChain: (string | number)[]; + /** The number of children for collection types */ + count?: number; + /** Names of children */ + hasNamedChildren?: boolean; + /** A method to get the children of this variable */ + getChildren?: (start: number, token: CancellationToken) => Promise; +} diff --git a/src/client/repl/variables/variableRequester.ts b/src/client/repl/variables/variableRequester.ts new file mode 100644 index 000000000000..e66afdcd6616 --- /dev/null +++ b/src/client/repl/variables/variableRequester.ts @@ -0,0 +1,59 @@ +import { CancellationToken } from 'vscode'; +import path from 'path'; +import * as fsapi from '../../common/platform/fs-paths'; +import { IVariableDescription } from './types'; +import { PythonServer } from '../pythonServer'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +const VARIABLE_SCRIPT_LOCATION = path.join(EXTENSION_ROOT_DIR, 'python_files', 'get_variable_info.py'); + +export class VariableRequester { + public static scriptContents: string | undefined; + + constructor(private pythonServer: PythonServer) {} + + async getAllVariableDescriptions( + parent: IVariableDescription | undefined, + start: number, + token: CancellationToken, + ): Promise { + const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); + if (parent) { + const printCall = `import json;return json.dumps(getAllChildrenDescriptions(\'${ + parent.root + }\', ${JSON.stringify(parent.propertyChain)}, ${start}))`; + scriptLines.push(printCall); + } else { + scriptLines.push('import json;return json.dumps(getVariableDescriptions())'); + } + + if (token.isCancellationRequested) { + return []; + } + + const script = wrapScriptInFunction(scriptLines); + const result = await this.pythonServer.executeSilently(script); + + if (result?.output && !token.isCancellationRequested) { + return JSON.parse(result.output) as IVariableDescription[]; + } + + return []; + } +} + +function wrapScriptInFunction(scriptLines: string[]): string { + const indented = scriptLines.map((line) => ` ${line}`).join('\n'); + // put everything into a function scope and then delete that scope + // TODO: run in a background thread + return `def __VSCODE_run_script():\n${indented}\nprint(__VSCODE_run_script())\ndel __VSCODE_run_script`; +} + +async function getContentsOfVariablesScript(): Promise { + if (VariableRequester.scriptContents) { + return VariableRequester.scriptContents; + } + const contents = await fsapi.readFile(VARIABLE_SCRIPT_LOCATION, 'utf-8'); + VariableRequester.scriptContents = contents; + return VariableRequester.scriptContents; +} diff --git a/src/client/repl/variables/variableResultCache.ts b/src/client/repl/variables/variableResultCache.ts new file mode 100644 index 000000000000..1e19415becb7 --- /dev/null +++ b/src/client/repl/variables/variableResultCache.ts @@ -0,0 +1,28 @@ +import { VariablesResult } from 'vscode'; + +export class VariableResultCache { + private cache = new Map(); + + private executionCount = 0; + + getResults(executionCount: number, cacheKey: string): VariablesResult[] | undefined { + if (this.executionCount !== executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } + + return this.cache.get(cacheKey); + } + + setResults(executionCount: number, cacheKey: string, results: VariablesResult[]): void { + if (this.executionCount < executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } else if (this.executionCount > executionCount) { + // old results, don't cache + return; + } + + this.cache.set(cacheKey, results); + } +} diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts new file mode 100644 index 000000000000..f033451dc80e --- /dev/null +++ b/src/client/repl/variables/variablesProvider.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + CancellationToken, + NotebookDocument, + Variable, + NotebookVariablesRequestKind, + VariablesResult, + EventEmitter, + Event, + NotebookVariableProvider, + Uri, +} from 'vscode'; +import { VariableResultCache } from './variableResultCache'; +import { IVariableDescription } from './types'; +import { VariableRequester } from './variableRequester'; +import { getConfiguration } from '../../common/vscodeApis/workspaceApis'; + +export class VariablesProvider implements NotebookVariableProvider { + private readonly variableResultCache = new VariableResultCache(); + + private _onDidChangeVariables = new EventEmitter(); + + onDidChangeVariables = this._onDidChangeVariables.event; + + private executionCount = 0; + + constructor( + private readonly variableRequester: VariableRequester, + private readonly getNotebookDocument: () => NotebookDocument | undefined, + codeExecutedEvent: Event, + ) { + codeExecutedEvent(() => this.onDidExecuteCode()); + } + + onDidExecuteCode(): void { + const notebook = this.getNotebookDocument(); + if (notebook) { + this.executionCount += 1; + if (isEnabled(notebook.uri)) { + this._onDidChangeVariables.fire(notebook); + } + } + } + + async *provideVariables( + notebook: NotebookDocument, + parent: Variable | undefined, + kind: NotebookVariablesRequestKind, + start: number, + token: CancellationToken, + ): AsyncIterable { + const notebookDocument = this.getNotebookDocument(); + if ( + !isEnabled(notebook.uri) || + token.isCancellationRequested || + !notebookDocument || + notebookDocument !== notebook + ) { + return; + } + + const { executionCount } = this; + const cacheKey = getVariableResultCacheKey(notebook.uri.toString(), parent, start); + let results = this.variableResultCache.getResults(executionCount, cacheKey); + + if (parent) { + const parentDescription = parent as IVariableDescription; + if (!results && parentDescription.getChildren) { + const variables = await parentDescription.getChildren(start, token); + if (token.isCancellationRequested) { + return; + } + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } else if (!results) { + // no cached results and no way to get children, so return empty + return; + } + + for (const result of results) { + yield result; + } + + // check if we have more indexed children to return + if ( + kind === 2 && + parentDescription.count && + results.length > 0 && + parentDescription.count > start + results.length + ) { + for await (const result of this.provideVariables( + notebook, + parent, + kind, + start + results.length, + token, + )) { + yield result; + } + } + } else { + if (!results) { + const variables = await this.variableRequester.getAllVariableDescriptions(undefined, start, token); + if (token.isCancellationRequested) { + return; + } + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } + + for (const result of results) { + yield result; + } + } + } + + private createVariableResult(result: IVariableDescription): VariablesResult { + const indexedChildrenCount = result.count ?? 0; + const hasNamedChildren = !!result.hasNamedChildren; + const variable = { + getChildren: (start: number, token: CancellationToken) => this.getChildren(variable, start, token), + expression: createExpression(result.root, result.propertyChain), + ...result, + } as Variable; + return { variable, hasNamedChildren, indexedChildrenCount }; + } + + async getChildren(variable: Variable, start: number, token: CancellationToken): Promise { + const parent = variable as IVariableDescription; + return this.variableRequester.getAllVariableDescriptions(parent, start, token); + } +} + +function createExpression(root: string, propertyChain: (string | number)[]): string { + let expression = root; + for (const property of propertyChain) { + if (typeof property === 'string') { + expression += `.${property}`; + } else { + expression += `[${property}]`; + } + } + return expression; +} + +function getVariableResultCacheKey(uri: string, parent: Variable | undefined, start: number) { + let parentKey = ''; + const parentDescription = parent as IVariableDescription; + if (parentDescription) { + parentKey = `${parentDescription.name}.${parentDescription.propertyChain.join('.')}[[${start}`; + } + return `${uri}:${parentKey}`; +} + +function isEnabled(resource?: Uri) { + return getConfiguration('python', resource).get('REPL.provideVariables'); +} diff --git a/src/client/sourceMapSupport.ts b/src/client/sourceMapSupport.ts deleted file mode 100644 index 0303e1e43a93..000000000000 --- a/src/client/sourceMapSupport.ts +++ /dev/null @@ -1,86 +0,0 @@ -// 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 index 8b270dc95967..f7a2a6aea517 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -1,40 +1,29 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as vscode from 'vscode'; 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 { IInterpreterPathService, Resource } from './common/types'; +import { IStopWatch } from './common/utils/stopWatch'; +import { IInterpreterAutoSelectionService } from './interpreter/autoSelection/types'; import { ICondaService, IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; -import { PythonInterpreter } from './pythonEnvironments/info'; +import { traceError } from './logging'; +import { EnvironmentType, PythonEnvironment } from './pythonEnvironments/info'; import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; import { EditorLoadTelemetry } from './telemetry/types'; - -interface IStopWatch { - elapsedTime: number; -} +import { IStartupDurations } from './types'; +import { useEnvExtension } from './envExt/api.internal'; export async function sendStartupTelemetry( - // tslint:disable-next-line:no-any activatedPromise: Promise, - durations: Record, + durations: IStartupDurations, stopWatch: IStopWatch, - serviceContainer: IServiceContainer + serviceContainer: IServiceContainer, + isFirstSession: boolean, ) { if (isTestExecution()) { return; @@ -42,8 +31,8 @@ export async function sendStartupTelemetry( try { await activatedPromise; - durations.totalActivateTime = stopWatch.elapsedTime; - const props = await getActivationTelemetryProps(serviceContainer); + durations.totalNonBlockingActivateTime = stopWatch.elapsedTime - durations.startActivateTime; + const props = await getActivationTelemetryProps(serviceContainer, isFirstSession); sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props); } catch (ex) { traceError('sendStartupTelemetry() failed.', ex); @@ -52,11 +41,10 @@ export async function sendStartupTelemetry( export async function sendErrorTelemetry( ex: Error, - durations: Record, - serviceContainer?: IServiceContainer + durations: IStartupDurations, + serviceContainer?: IServiceContainer, ) { try { - // tslint:disable-next-line:no-any let props: any = {}; if (serviceContainer) { try { @@ -81,16 +69,8 @@ function isUsingGlobalInterpreterInWorkspace(currentPythonPath: string, serviceC } 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); + let settings = interpreterPathService.inspect(resource); return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || (settings.workspaceValue && settings.workspaceValue !== 'python') || (settings.globalValue && settings.globalValue !== 'python') @@ -98,51 +78,65 @@ export function hasUserDefinedPythonPath(resource: Resource, serviceContainer: I : 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 +async function getActivationTelemetryProps( + serviceContainer: IServiceContainer, + isFirstSession?: boolean, +): Promise { // 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 appName = vscode.env.appName; + const workspaceService = serviceContainer.get(IWorkspaceService); + const workspaceFolderCount = workspaceService.workspaceFolders?.length || 0; const terminalHelper = serviceContainer.get(ITerminalHelper); const terminalShellType = terminalHelper.identifyTerminalShell(); - const condaLocator = serviceContainer.get(ICondaService); + if (!workspaceService.isTrusted) { + return { workspaceFolderCount, terminal: terminalShellType, isFirstSession }; + } const interpreterService = serviceContainer.get(IInterpreterService); - const workspaceService = serviceContainer.get(IWorkspaceService); - const configurationService = serviceContainer.get(IConfigurationService); - const mainWorkspaceUri = workspaceService.hasWorkspaceFolders - ? workspaceService.workspaceFolders![0].uri + const mainWorkspaceUri = workspaceService.workspaceFolders?.length + ? workspaceService.workspaceFolders[0].uri : undefined; - const settings = configurationService.getSettings(mainWorkspaceUri); - const [condaVersion, interpreter, interpreters] = await Promise.all([ - condaLocator + const hasPythonThree = await interpreterService.hasInterpreters(async (item) => item.version?.major === 3); + // If an unknown type environment can be found from windows registry or path env var, + // consider them as global type instead of unknown. Such types can only be known after + // windows registry is queried. So wait for the refresh of windows registry locator to + // finish. API getActiveInterpreter() does not block on windows registry by default as + // it is slow. + await interpreterService.refreshPromise; + let interpreter: PythonEnvironment | undefined; + + // include main workspace uri if using env extension + if (useEnvExtension()) { + interpreter = await interpreterService + .getActiveInterpreter(mainWorkspaceUri) + .catch(() => undefined); + } else { + interpreter = await interpreterService + .getActiveInterpreter() + .catch(() => undefined); + } + + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; + const interpreterType = interpreter ? interpreter.envType : undefined; + if (interpreterType === EnvironmentType.Unknown) { + traceError('Active interpreter type is detected as Unknown', JSON.stringify(interpreter)); + } + let condaVersion = undefined; + if (interpreterType === EnvironmentType.Conda) { + const condaLocator = serviceContainer.get(ICondaService); + condaVersion = await 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.type : undefined; + .catch(() => ''); + } const usingUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); - const preferredWorkspaceInterpreter = getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer); - const usingGlobalInterpreter = isUsingGlobalInterpreterInWorkspace(settings.pythonPath, serviceContainer); - const usingAutoSelectedWorkspaceInterpreter = preferredWorkspaceInterpreter - ? settings.pythonPath === getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer) + const usingGlobalInterpreter = interpreter + ? isUsingGlobalInterpreterInWorkspace(interpreter.path, serviceContainer) : false; - const hasPython3 = - interpreters!.filter((item) => (item && item.version ? item.version.major === 3 : false)).length > 0; + const usingEnvironmentsExtension = useEnvExtension(); return { condaVersion, @@ -150,9 +144,11 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): pythonVersion, interpreterType, workspaceFolderCount, - hasPython3, + hasPythonThree, usingUserDefinedInterpreter, - usingAutoSelectedWorkspaceInterpreter, - usingGlobalInterpreter + usingGlobalInterpreter, + appName, + isFirstSession, + usingEnvironmentsExtension, }; } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index f0f38e6ac69a..eff32a6e3299 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -4,114 +4,104 @@ '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', + INVOKE_TOOL = 'INVOKE_TOOL', + CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND', SELECT_INTERPRETER = 'SELECT_INTERPRETER', SELECT_INTERPRETER_ENTER_BUTTON = 'SELECT_INTERPRETER_ENTER_BUTTON', SELECT_INTERPRETER_ENTER_CHOICE = 'SELECT_INTERPRETER_ENTER_CHOICE', + SELECT_INTERPRETER_SELECTED = 'SELECT_INTERPRETER_SELECTED', + SELECT_INTERPRETER_ENTER_OR_FIND = 'SELECT_INTERPRETER_ENTER_OR_FIND', + SELECT_INTERPRETER_ENTERED_EXISTS = 'SELECT_INTERPRETER_ENTERED_EXISTS', PYTHON_INTERPRETER = 'PYTHON_INTERPRETER', PYTHON_INSTALL_PACKAGE = 'PYTHON_INSTALL_PACKAGE', + ENVIRONMENT_WITHOUT_PYTHON_SELECTED = 'ENVIRONMENT_WITHOUT_PYTHON_SELECTED', + PYTHON_ENVIRONMENTS_API = 'PYTHON_ENVIRONMENTS_API', PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY', + NATIVE_FINDER_MISSING_CONDA_ENVS = 'NATIVE_FINDER_MISSING_CONDA_ENVS', + NATIVE_FINDER_MISSING_POETRY_ENVS = 'NATIVE_FINDER_MISSING_POETRY_ENVS', + NATIVE_FINDER_PERF = 'NATIVE_FINDER_PERF', + PYTHON_INTERPRETER_DISCOVERY_INVALID_NATIVE = 'PYTHON_INTERPRETER_DISCOVERY_INVALID_NATIVE', PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION', - PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES', + 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', + REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', + ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', 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', + // Python testing specific telemetry + UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', + UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE', + UNITTEST_DISCOVERY_TRIGGER = 'UNITTEST.DISCOVERY.TRIGGER', + UNITTEST_DISCOVERING = 'UNITTEST.DISCOVERING', + UNITTEST_DISCOVERING_STOP = 'UNITTEST.DISCOVERY.STOP', + UNITTEST_DISCOVERY_DONE = 'UNITTEST.DISCOVERY.DONE', + UNITTEST_RUN_STOP = 'UNITTEST.RUN.STOP', + UNITTEST_RUN = 'UNITTEST.RUN', + UNITTEST_RUN_ALL_FAILED = 'UNITTEST.RUN_ALL_FAILED', + UNITTEST_DISABLED = 'UNITTEST.DISABLED', - 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_EXPERIMENTS_INIT_PERFORMANCE = 'PYTHON_EXPERIMENTS_INIT_PERFORMANCE', + PYTHON_EXPERIMENTS_LSP_NOTEBOOKS = 'PYTHON_EXPERIMENTS_LSP_NOTEBOOKS', + PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS = 'PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS', - 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', + EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT', LANGUAGE_SERVER_ENABLED = 'LANGUAGE_SERVER.ENABLED', + LANGUAGE_SERVER_TRIGGER_TIME = 'LANGUAGE_SERVER_TRIGGER_TIME', 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_RESTART = 'LANGUAGE_SERVER.RESTART', 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', + USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', - 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' + JEDI_LANGUAGE_SERVER_ENABLED = 'JEDI_LANGUAGE_SERVER.ENABLED', + JEDI_LANGUAGE_SERVER_STARTUP = 'JEDI_LANGUAGE_SERVER.STARTUP', + JEDI_LANGUAGE_SERVER_READY = 'JEDI_LANGUAGE_SERVER.READY', + JEDI_LANGUAGE_SERVER_REQUEST = 'JEDI_LANGUAGE_SERVER.REQUEST', + + TENSORBOARD_INSTALL_PROMPT_SHOWN = 'TENSORBOARD.INSTALL_PROMPT_SHOWN', + TENSORBOARD_INSTALL_PROMPT_SELECTION = 'TENSORBOARD.INSTALL_PROMPT_SELECTION', + TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL = 'TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL', + TENSORBOARD_PACKAGE_INSTALL_RESULT = 'TENSORBOARD.PACKAGE_INSTALL_RESULT', + TENSORBOARD_TORCH_PROFILER_IMPORT = 'TENSORBOARD.TORCH_PROFILER_IMPORT', + + ENVIRONMENT_CREATING = 'ENVIRONMENT.CREATING', + ENVIRONMENT_CREATED = 'ENVIRONMENT.CREATED', + ENVIRONMENT_FAILED = 'ENVIRONMENT.FAILED', + ENVIRONMENT_INSTALLING_PACKAGES = 'ENVIRONMENT.INSTALLING_PACKAGES', + ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', + ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', + ENVIRONMENT_BUTTON = 'ENVIRONMENT.BUTTON', + ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', + ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', + + ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', + ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', + ENVIRONMENT_TERMINAL_GLOBAL_PIP = 'ENVIRONMENT.TERMINAL.GLOBAL_PIP', } export enum PlatformErrors { FailedToParseVersion = 'FailedToParseVersion', - FailedToDetermineOS = 'FailedToDetermineOS' + FailedToDetermineOS = 'FailedToDetermineOS', } diff --git a/src/client/telemetry/envFileTelemetry.ts b/src/client/telemetry/envFileTelemetry.ts index dfb30d851297..bf76a08733f6 100644 --- a/src/client/telemetry/envFileTelemetry.ts +++ b/src/client/telemetry/envFileTelemetry.ts @@ -14,13 +14,13 @@ import { EventName } from './constants'; let _defaultEnvFileSetting: string | undefined; let envFileTelemetrySent = false; -export function sendSettingTelemetry(workspaceService: IWorkspaceService, envFileSetting?: string) { +export function sendSettingTelemetry(workspaceService: IWorkspaceService, envFileSetting?: string): void { if (shouldSendTelemetry() && envFileSetting !== defaultEnvFileSetting(workspaceService)) { sendTelemetry(true); } } -export function sendFileCreationTelemetry() { +export function sendFileCreationTelemetry(): void { if (shouldSendTelemetry()) { sendTelemetry(); } @@ -29,8 +29,8 @@ export function sendFileCreationTelemetry() { export async function sendActivationTelemetry( fileSystem: IFileSystem, workspaceService: IWorkspaceService, - resource: Resource -) { + resource: Resource, +): Promise { if (shouldSendTelemetry()) { const systemVariables = new SystemVariables(resource, undefined, workspaceService); const envFilePath = systemVariables.resolveAny(defaultEnvFileSetting(workspaceService))!; @@ -42,7 +42,7 @@ export async function sendActivationTelemetry( } } -function sendTelemetry(hasCustomEnvPath: boolean = false) { +function sendTelemetry(hasCustomEnvPath = false) { sendTelemetryEvent(EventName.ENVFILE_WORKSPACE, undefined, { hasCustomEnvPath }); envFileTelemetrySent = true; @@ -62,18 +62,17 @@ function defaultEnvFileSetting(workspaceService: IWorkspaceService) { } // Set state for tests. -export namespace EnvFileTelemetryTests { - export function setState({ telemetrySent, defaultSetting }: { telemetrySent?: boolean; defaultSetting?: string }) { +export const EnvFileTelemetryTests = { + setState: ({ telemetrySent, defaultSetting }: { telemetrySent?: boolean; defaultSetting?: string }): void => { if (telemetrySent !== undefined) { envFileTelemetrySent = telemetrySent; } if (defaultEnvFileSetting !== undefined) { _defaultEnvFileSetting = defaultSetting; } - } - - export function resetState() { + }, + resetState: (): void => { _defaultEnvFileSetting = undefined; envFileTelemetrySent = false; - } -} + }, +}; diff --git a/src/client/telemetry/extensionInstallTelemetry.ts b/src/client/telemetry/extensionInstallTelemetry.ts new file mode 100644 index 000000000000..ea012b694971 --- /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): Promise { + // 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 index 44fa02794b60..cf8e1ed48837 100644 --- a/src/client/telemetry/importTracker.ts +++ b/src/client/telemetry/importTracker.ts @@ -1,18 +1,22 @@ +/* eslint-disable class-methods-use-this */ // 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 { clearTimeout, setTimeout } from 'timers'; import { TextDocument } from 'vscode'; -import { captureTelemetry, sendTelemetryEvent } from '.'; -import { splitMultilineString } from '../../datascience-ui/common'; +import { createHash } from 'crypto'; +import { sendTelemetryEvent } from '.'; import { IExtensionSingleActivationService } from '../activation/types'; import { IDocumentManager } from '../common/application/types'; import { isTestExecution } from '../common/constants'; import '../common/extensions'; +import { IDisposableRegistry } from '../common/types'; import { noop } from '../common/utils/misc'; -import { ICell, INotebookEditor, INotebookEditorProvider, INotebookExecutionLogger } from '../datascience/types'; +import { TorchProfilerImportRegEx } from '../tensorBoard/helpers'; import { EventName } from './constants'; /* @@ -44,91 +48,41 @@ const MAX_DOCUMENT_LINES = 1000; 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; +export class ImportTracker implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + private pendingChecks = new Map(); + + private static sentMatches: Set = new Set(); constructor( @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(INotebookEditorProvider) private notebookEditorProvider: INotebookEditorProvider + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, ) { - 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)); + this.documentManager.onDidOpenTextDocument((t) => this.onOpenedOrSavedDocument(t), this, this.disposables); + this.documentManager.onDidSaveTextDocument((t) => this.onOpenedOrSavedDocument(t), this, this.disposables); } - public dispose() { + public dispose(): void { 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, '')); + public static hasModuleImport(moduleName: string): boolean { + return this.sentMatches.has(moduleName); } private onOpenedOrSavedDocument(document: TextDocument) { // Make sure this is a Python file. - if (path.extname(document.fileName) === '.py') { + if (path.extname(document.fileName).toLowerCase() === '.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)); } @@ -137,8 +91,7 @@ export class ImportTracker implements IExtensionSingleActivationService, INotebo // If already scheduled, cancel. const currentTimeout = this.pendingChecks.get(file); if (currentTimeout) { - // tslint:disable-next-line: no-any - clearTimeout(currentTimeout as any); + clearTimeout(currentTimeout); this.pendingChecks.delete(file); } @@ -152,40 +105,21 @@ export class ImportTracker implements IExtensionSingleActivationService, INotebo } } - 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); + const lines = 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)) { + if (ImportTracker.sentMatches.has(packageName)) { return; } - this.sentMatches.add(packageName); + ImportTracker.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'); + const hash = createHash('sha256').update(packageName).digest('hex'); sendTelemetryEvent(EventName.HASHED_PACKAGE_NAME, undefined, { hashedName: hash }); } @@ -206,6 +140,9 @@ export class ImportTracker implements IExtensionSingleActivationService, INotebo packageNames.forEach((p) => this.sendTelemetry(p)); } } + if (s && TorchProfilerImportRegEx.test(s)) { + sendTelemetryEvent(EventName.TENSORBOARD_TORCH_PROFILER_IMPORT); + } } } catch { // Don't care about failures since this is just telemetry. @@ -213,3 +150,16 @@ export class ImportTracker implements IExtensionSingleActivationService, INotebo } } } + +export function 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); +} diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index a8f973bca77f..738c5f8a2776 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1,66 +1,51 @@ +/* eslint-disable global-require */ // 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 TelemetryReporter from '@vscode/extension-telemetry'; +import type * as vscodeTypes from 'vscode'; 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 type { TerminalShellType } from '../common/terminal/types'; +import { isPromise } from '../common/utils/async'; 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 { InterpreterType } from '../pythonEnvironments/info'; -import { TestProvider } from '../testing/common/types'; -import { EventName, PlatformErrors } from './constants'; -import { LinterTrigger, TestTool } from './types'; - -// tslint:disable: no-any +import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; +import { TensorBoardPromptSelection } from '../tensorBoard/constants'; +import { EventName } from './constants'; +import type { TestTool } from './types'; /** * 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'); + const reporter = require('@vscode/extension-telemetry'); + return vsc !== undefined && reporter !== undefined; } catch { return false; } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let packageJSON: any; + /** - * Checks if the telemetry is disabled in user settings - * @returns {boolean} + * Checks if the telemetry is disabled */ -export function isTelemetryDisabled(workspaceService: IWorkspaceService): boolean { - const settings = workspaceService.getConfiguration('telemetry').inspect('enableTelemetry')!; - return settings.globalValue === false ? true : false; +export function isTelemetryDisabled(): boolean { + if (!packageJSON) { + const vscode = require('vscode') as typeof vscodeTypes; + const pythonExtension = vscode.extensions.getExtension(PVSC_EXTENSION_ID)!; + packageJSON = pythonExtension.packageJSON; + } + return !packageJSON.enableTelemetry; } -const sharedProperties: Record = {}; +const sharedProperties: Record = {}; /** * Set shared properties for all telemetry events. */ @@ -87,103 +72,106 @@ export function _resetSharedProperties(): void { } let telemetryReporter: TelemetryReporter | undefined; -function getTelemetryReporter() { +export function getTelemetryReporter(): TelemetryReporter { 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)); + const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; + telemetryReporter = new Reporter(AppinsightsKey, [ + { + lookup: /(errorName|errorMessage|errorStack)/g, + }, + ]); + + return telemetryReporter; } -export function clearTelemetryReporter() { +export function clearTelemetryReporter(): void { telemetryReporter = undefined; } export function sendTelemetryEvent

( eventName: E, - durationMs?: Record | number, + measuresOrDurationMs?: Record | number, properties?: P[E], - ex?: Error -) { - if (isTestExecution() || !isTelemetrySupported()) { + ex?: Error, +): void { + if (isTestExecution() || !isTelemetrySupported() || isTelemetryDisabled()) { return; } const reporter = getTelemetryReporter(); - const measures = typeof durationMs === 'number' ? { duration: durationMs } : durationMs ? durationMs : undefined; - let customProperties: Record = {}; - let eventNameSent = eventName as string; + const measures = + typeof measuresOrDurationMs === 'number' + ? { duration: measuresOrDurationMs } + : measuresOrDurationMs || undefined; + const customProperties: Record = {}; + const 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); + if (properties) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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. + switch (typeof data[prop]) { + case 'string': + customProperties[prop] = data[prop]; + break; + case 'object': + customProperties[prop] = 'object'; + break; + default: + customProperties[prop] = data[prop].toString(); + break; } - }); - } - - // 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]; + } catch (exception) { + console.error(`Failed to serialize ${prop} for ${String(eventName)}`, exception); // use console due to circular dependencies with trace calls } }); + } + // Add shared properties to telemetry props (we may overwrite existing ones). + Object.assign(customProperties, sharedProperties); + + if (ex) { + const errorProps = { + errorName: ex.name, + errorStack: ex.stack ?? '', + }; + Object.assign(customProperties, errorProps); + reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures); + } else { reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); } if (process.env && process.env.VSC_PYTHON_LOG_TELEMETRY) { - traceInfo( + console.info( `Telemetry Event : ${eventNameSent} Measures: ${JSON.stringify(measures)} Props: ${JSON.stringify( - customProperties - )} ` - ); + customProperties, + )} `, + ); // use console due to circular dependencies with trace calls } } // Type-parameterized form of MethodDecorator in lib.es5.d.ts. type TypedMethodDescriptor = ( - target: Object, + target: unknown, propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor + descriptor: TypedPropertyDescriptor, ) => TypedPropertyDescriptor | void; +// The following code uses "any" in many places, as TS does not have rich support +// for typing decorators. Specifically, while it is possible to write types which +// encode the signature of the wrapped function, TS fails to actually infer the +// type of "this" and the signature at call sites, instead choosing to infer +// based on other hints (like the closure parameters), which ends up making it +// no safer than "any" (and sometimes misleading enough to be more unsafe). + /** * Decorates a method, sending a telemetry event with the given properties. * @param eventName The event name to send. @@ -192,67 +180,72 @@ type TypedMethodDescriptor = ( * @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). + * @param lazyMeasures A static function on the decorated class which returns extra measures to add to the event. + * This can be used to provide measures 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, + captureDuration = true, failureEventName?: E, - lazyProperties?: (obj: This) => P[E] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lazyProperties?: (obj: This, result?: any) => P[E], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lazyMeasures?: (obj: This, result?: any) => Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): TypedMethodDescriptor<(this: This, ...args: any[]) => any> { - // tslint:disable-next-line:no-function-expression no-any return function ( - _target: Object, + _target: unknown, _propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor<(this: This, ...args: any[]) => any> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor: TypedPropertyDescriptor<(this: This, ...args: any[]) => any>, ) { const originalMethod = descriptor.value!; - // tslint:disable-next-line:no-function-expression no-any + + // eslint-disable-next-line @typescript-eslint/no-explicit-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) { + if (!captureDuration && !lazyProperties && !lazyMeasures) { sendTelemetryEvent(eventName, undefined, properties); - // tslint:disable-next-line:no-invalid-this + return originalMethod.apply(this, args); } - const props = () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getProps = (result?: any) => { if (lazyProperties) { - return { ...properties, ...lazyProperties(this) }; + return { ...properties, ...lazyProperties(this, result) }; } return properties; }; const stopWatch = captureDuration ? new StopWatch() : undefined; - // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getMeasures = (result?: any) => { + const measures = stopWatch ? { duration: stopWatch.elapsedTime } : undefined; + if (lazyMeasures) { + return { ...measures, ...lazyMeasures(this, result) }; + } + return measures; + }; + 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) + if (result && isPromise(result)) { + result .then((data) => { - sendTelemetryEvent(eventName, stopWatch?.elapsedTime, props()); + sendTelemetryEvent(eventName, getMeasures(data), getProps(data)); 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 - ); + const failedProps: P[E] = { ...getProps(), failed: true } as P[E] & FailedEventType; + sendTelemetryEvent(failureEventName || eventName, getMeasures(), failedProps, ex); }); } else { - sendTelemetryEvent(eventName, stopWatch?.elapsedTime, props()); + sendTelemetryEvent(eventName, getMeasures(result), getProps(result)); } return result; @@ -265,65 +258,27 @@ export function captureTelemetry(eventName: K, properties?: T[K]); export function sendTelemetryWhenDone

( eventName: E, - promise: Promise | Thenable, + promise: Promise | Thenable, stopWatch?: StopWatch, - properties?: P[E] -) { - stopWatch = stopWatch ? stopWatch : new StopWatch(); + properties?: P[E], +): void { + stopWatch = stopWatch || new StopWatch(); if (typeof promise.then === 'function') { - // tslint:disable-next-line:prefer-type-cast no-any - (promise as Promise).then( + (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. */ @@ -332,303 +287,18 @@ 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'; } +type FailedEventType = { failed: true }; + // 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. @@ -654,6 +324,11 @@ export interface IEventNamePropertyMapping { /** * Telemetry event sent when we are checking if we can handle the diagnostic code */ + /* __GDPR__ + "diagnostics.message" : { + "code" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.DIAGNOSTICS_MESSAGE]: { /** * Code of diagnostics message detected and displayed. @@ -664,19 +339,44 @@ export interface IEventNamePropertyMapping { /** * Telemetry event sent with details just after editor loads */ + /* __GDPR__ + "editor.load" : { + "appName" : {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud"}, + "codeloadingtime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "condaversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "luabud" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "luabud" }, + "pythonversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "interpretertype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "terminal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "luabud" }, + "workspacefoldercount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "haspythonthree" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "startactivatetime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "totalactivatetime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "totalnonblockingactivatetime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "usinguserdefinedinterpreter" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "usingglobalinterpreter" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "isfirstsession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } + } + */ [EventName.EDITOR_LOAD]: { + /** + * The name of the application where the Python extension is running + */ + appName?: string | undefined; /** * The conda version if selected */ - condaVersion: string | undefined; + condaVersion?: string | undefined; /** * The python interpreter version if selected */ - pythonVersion: string | undefined; + pythonVersion?: string | undefined; /** * The type of interpreter (conda, virtualenv, pipenv etc.) */ - interpreterType: InterpreterType | undefined; + interpreterType?: EnvironmentType | undefined; /** * The type of terminal shell created: powershell, cmd, zsh, bash etc. * @@ -690,27 +390,41 @@ export interface IEventNamePropertyMapping { /** * If interpreters found for the main workspace contains a python3 interpreter */ - hasPython3: boolean; + hasPythonThree?: boolean; /** * If user has defined an interpreter in settings.json */ - usingUserDefinedInterpreter: boolean; + usingUserDefinedInterpreter?: boolean; /** - * If interpreter is auto selected for the workspace + * If global interpreter is being used */ - usingAutoSelectedWorkspaceInterpreter: boolean; + usingGlobalInterpreter?: boolean; /** - * If global interpreter is being used + * Carries `true` if it is the very first session of the user. We check whether persistent cache is empty + * to approximately guess if it's the first session. + */ + isFirstSession?: boolean; + /** + * If user has enabled the Python Environments extension integration */ - usingGlobalInterpreter: boolean; + usingEnvironmentsExtension?: boolean; }; /** * Telemetry event sent when substituting Environment variables to calculate value of variables */ + /* __GDPR__ + "envfile_variable_substitution" : { "owner": "karthiknadig" } + */ [EventName.ENVFILE_VARIABLE_SUBSTITUTION]: never | undefined; /** * Telemetry event sent when an environment file is detected in the workspace. */ + /* __GDPR__ + "envfile_workspace" : { + "hascustomenvpath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } + } + */ + [EventName.ENVFILE_WORKSPACE]: { /** * If there's a custom path specified in the python.envFile workspace settings. @@ -721,19 +435,31 @@ export interface IEventNamePropertyMapping { * Telemetry Event sent when user sends code to be executed in the terminal. * */ + /* __GDPR__ + "execution_code" : { + "scope" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.EXECUTION_CODE]: { /** - * Whether the user executed a file in the terminal or just the selected text. + * Whether the user executed a file in the terminal or just the selected text or line by shift+enter. * * @type {('file' | 'selection')} */ - scope: 'file' | 'selection'; + scope: 'file' | 'selection' | 'line'; /** * How was the code executed (through the command or by clicking the `Run File` icon). * * @type {('command' | 'icon')} */ trigger?: 'command' | 'icon'; + /** + * Whether user chose to execute this Python file in a separate terminal or not. + * + * @type {boolean} + */ + newTerminalPerFile?: boolean; }; /** * Telemetry Event sent when user executes code against Django Shell. @@ -741,6 +467,11 @@ export interface IEventNamePropertyMapping { * scope * */ + /* __GDPR__ + "execution_django" : { + "scope" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.EXECUTION_DJANGO]: { /** * If `file`, then the file was executed in the django shell. @@ -750,26 +481,15 @@ export interface IEventNamePropertyMapping { */ 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' */ + /* __GDPR__ + "format.format_on_type" : { + "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.FORMAT_ON_TYPE]: { /** * Carries `true` if format on type is enabled, `false` otherwise @@ -778,21 +498,15 @@ export interface IEventNamePropertyMapping { */ 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 */ + /* __GDPR__ + "hashed_package_name" : { + "hashedname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } + } + */ [EventName.HASHED_PACKAGE_NAME]: { /** * Hash of the package name @@ -801,127 +515,80 @@ export interface IEventNamePropertyMapping { */ 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 */ + /* __GDPR__ + "python_install_package" : { + "installer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "requiredinstaller" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "productname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "isinstalled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "envtype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.PYTHON_INSTALL_PACKAGE]: { /** * The name of the module. (pipenv, Conda etc.) - * - * @type {string} + * One of the possible values includes `unavailable`, meaning user doesn't have pip, conda, or other tools available that can be used to install a python package. */ installer: string; - }; - /** - * Telemetry sent with details immediately after linting a document completes - */ - [EventName.LINTING]: { /** - * Name of the linter being used - * - * @type {LinterId} + * The name of the installer required (expected to be available) for installation of packages. (pipenv, Conda etc.) */ - tool: LinterId; + requiredInstaller?: string; /** - * If custom arguments for linter is provided in settings.json - * - * @type {boolean} + * Name of the corresponding product (package) to be installed. */ - hasCustomArgs: boolean; + productName?: string; /** - * Carries the source which triggered configuration of tests - * - * @type {LinterTrigger} + * Whether the product (package) has been installed or not. */ - trigger: LinterTrigger; + isInstalled?: boolean; /** - * Carries `true` if linter executable is specified, `false` otherwise - * - * @type {boolean} + * Type of the Python environment into which the Python package is being installed. */ - executableSpecified: boolean; - }; - /** - * Telemetry event sent after fetching the OS version - */ - [EventName.PLATFORM_INFO]: { + envType?: PythonEnvironment['envType']; /** - * If fetching OS version fails, list the failure type - * - * @type {PlatformErrors} - */ - failureType?: PlatformErrors; - /** - * The OS version of the platform - * - * @type {string} + * Version of the Python environment into which the Python package is being installed. */ - osVersion?: string; + version?: string; }; /** - * Telemetry is sent with details about the play run file icon + * Telemetry event sent when an environment without contain a python binary is selected. */ - [EventName.PLAY_BUTTON_ICON_DISABLED]: { - /** - * Carries `true` if play button icon is not shown (because code runner is installed), `false` otherwise - */ - disabled: boolean; - }; + /* __GDPR__ + "environment_without_python_selected" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_WITHOUT_PYTHON_SELECTED]: never | undefined; /** * Telemetry event sent when 'Select Interpreter' command is invoked. */ + /* __GDPR__ + "select_interpreter" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ [EventName.SELECT_INTERPRETER]: never | undefined; /** * Telemetry event sent when 'Enter interpreter path' button is clicked. */ + /* __GDPR__ + "select_interpreter_enter_button" : { "owner": "karthiknadig" } + */ [EventName.SELECT_INTERPRETER_ENTER_BUTTON]: never | undefined; /** * Telemetry event sent with details about what choice user made to input the interpreter path. */ + /* __GDPR__ + "select_interpreter_enter_choice" : { + "choice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.SELECT_INTERPRETER_ENTER_CHOICE]: { /** * Carries 'enter' if user chose to enter the path to executable. @@ -929,9 +596,76 @@ export interface IEventNamePropertyMapping { */ choice: 'enter' | 'browse'; }; + /** + * Telemetry event sent after an action has been taken while the interpreter quickpick was displayed, + * and if the action was not 'Enter interpreter path'. + */ + /* __GDPR__ + "select_interpreter_selected" : { + "action" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.SELECT_INTERPRETER_SELECTED]: { + /** + * 'escape' if the quickpick was dismissed. + * 'selected' if an interpreter was selected. + */ + action: 'escape' | 'selected'; + }; + /** + * Telemetry event sent when the user select to either enter or find the interpreter from the quickpick. + */ + /* __GDPR__ + "select_interpreter_enter_or_find" : { "owner": "karthiknadig" } + */ + + [EventName.SELECT_INTERPRETER_ENTER_OR_FIND]: never | undefined; + /** + * Telemetry event sent after the user entered an interpreter path, or found it by browsing the filesystem. + */ + /* __GDPR__ + "select_interpreter_entered_exists" : { + "discovered" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.SELECT_INTERPRETER_ENTERED_EXISTS]: { + /** + * Carries `true` if the interpreter that was selected had already been discovered earlier (exists in the cache). + */ + discovered: boolean; + }; + + /** + * Telemetry event sent when another extension calls into python extension's environment API. Contains details + * of the other extension. + */ + /* __GDPR__ + "python_environments_api" : { + "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false , "owner": "karthiknadig"}, + "apiName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false, "owner": "karthiknadig" } + } + */ + [EventName.PYTHON_ENVIRONMENTS_API]: { + /** + * The ID of the extension calling the API. + */ + extensionId: string; + /** + * The name of the API called. + */ + apiName: string; + }; /** * Telemetry event sent with details after updating the python interpreter */ + /* __GDPR__ + "python_interpreter" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "pythonversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.PYTHON_INTERPRETER]: { /** * Carries the source which triggered the update @@ -951,19 +685,13 @@ export interface IEventNamePropertyMapping { * @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; }; + /* __GDPR__ + "python_interpreter.activation_environment_variables" : { + "hasenvvars" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ [EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES]: { /** * Carries `true` if environment variables are present, `false` otherwise @@ -977,23 +705,19 @@ export interface IEventNamePropertyMapping { * @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 */ + /* __GDPR__ + "python_interpreter_activation_for_running_code" : { + "hascommands" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "terminal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "pythonversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "interpretertype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE]: { /** * Carries `true` if activation commands exists for interpreter, `false` otherwise @@ -1022,13 +746,22 @@ export interface IEventNamePropertyMapping { /** * The type of the interpreter used * - * @type {InterpreterType} + * @type {EnvironmentType} */ - interpreterType: InterpreterType; + interpreterType: EnvironmentType; }; /** * Telemetry event sent when getting activation commands for terminal when interpreter is not specified */ + /* __GDPR__ + "python_interpreter_activation_for_terminal" : { + "hascommands" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "terminal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "pythonversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "interpretertype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL]: { /** * Carries `true` if activation commands exists for terminal, `false` otherwise @@ -1057,405 +790,1243 @@ export interface IEventNamePropertyMapping { /** * The type of the interpreter used * - * @type {InterpreterType} + * @type {EnvironmentType} */ - interpreterType: InterpreterType; + interpreterType: EnvironmentType; }; + /** + * Telemetry event sent when auto-selection is called. + */ + /* __GDPR__ + "python_interpreter_auto_selection" : { + "usecachedinterpreter" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.PYTHON_INTERPRETER_AUTO_SELECTION]: { /** - * The rule used to auto-select the interpreter + * If auto-selection has been run earlier in this session, and this call returned a cached value. * - * @type {AutoSelectionRule} + * @type {boolean} */ - rule?: AutoSelectionRule; + useCachedInterpreter?: boolean; + }; + /** + * Telemetry event sent when discovery of all python environments (virtualenv, conda, pipenv etc.) finishes. + */ + /* __GDPR__ + "python_interpreter_discovery" : { + "telVer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "workspaceFolderCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "condaInfoEnvsInvalid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "condaInfoEnvsDuplicate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "condaInfoEnvsInvalidPrefix" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "interpreters" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "envsWithDuplicatePrefixes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "envsNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "condaInfoEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "condaInfoEnvsDirs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "nativeCondaInfoEnvsDirs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "condaRcs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "nativeCondaRcs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "condaEnvsInEnvDir" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "condaEnvsInTxt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "nativeCondaEnvsInEnvDir" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "invalidCondaEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "prefixNotExistsCondaEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "condaEnvsWithoutPrefix" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true , "owner": "donjayamanne"}, + "environmentsWithoutPython" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "usingNativeLocator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "canSpawnConda" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "nativeCanSpawnConda" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne"}, + "userProvidedEnvFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixFoundInInfoNotInNative" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaDefaultPrefixFoundAsAnotherKind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixFoundAsPrefixOfAnother" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaDefaultPrefixFoundAsPrefixOfAnother" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixFoundInTxt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaDefaultPrefixFoundInTxt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixFoundInInfoAfterFind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixFoundInInfoAfterFindKind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixFoundAsAnotherKind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixInCondaExePath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaDefaultPrefixFoundInInfoNotInNative" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaDefaultPrefixFoundInInfoAfterFind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaDefaultPrefixFoundInInfoAfterFindKind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaDefaultPrefixInCondaExePath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "userProvidedCondaExe" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaRootPrefixEnvsAfterFind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "condaDefaultPrefixEnvsAfterFind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "activeStateEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "condaEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "customEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "hatchEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "microsoftStoreEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "otherGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "otherVirtualEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "pipEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "poetryEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "pyenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "systemEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "unknownEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "venvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "virtualEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "virtualEnvWrapperEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "global" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeEnvironmentsWithoutPython" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCondaEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCustomEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeMicrosoftStoreEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeOtherGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeOtherVirtualEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativePipEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativePoetryEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativePyenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeSystemEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeUnknownEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeVenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeVirtualEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeVirtualEnvWrapperEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeGlobal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeCondaEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeCustomEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeMicrosoftStoreEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeOtherVirtualEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativePipEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativePoetryEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativePyenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeSystemEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeUnknownEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeVenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeVirtualEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeVirtualEnvWrapperEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingNativeOtherGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCondaRcsNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCondaEnvDirsNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCondaEnvDirsNotFoundHasEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCondaEnvDirsNotFoundHasEnvsInTxt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCondaEnvTxtSame" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "nativeCondaEnvsFromTxt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "nativeCondaEnvTxtExists" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } + } + */ + [EventName.PYTHON_INTERPRETER_DISCOVERY]: { /** - * If cached interpreter no longer exists or is invalid - * - * @type {boolean} + * Version of this telemetry. */ - interpreterMissing?: boolean; + telVer?: number; /** - * Carries `true` if next rule is identified for autoselecting interpreter - * - * @type {boolean} + * Number of invalid envs returned by `conda info` */ - identified?: boolean; + condaInfoEnvsInvalid?: number; /** - * Carries `true` if cached interpreter is updated to use the current interpreter, `false` otherwise - * - * @type {boolean} + * Number of conda envs found in the environments.txt file. */ - updated?: boolean; - }; - /** - * Sends information regarding discovered python environments (virtualenv, conda, pipenv etc.) - */ - [EventName.PYTHON_INTERPRETER_DISCOVERY]: { + condaEnvsInTxt?: number; + /** + * Number of duplicate envs returned by `conda info` + */ + condaInfoEnvsDuplicate?: number; + /** + * Number of envs with invalid prefix returned by `conda info` + */ + condaInfoEnvsInvalidPrefix?: number; /** - * Name of the locator + * Number of workspaces. */ - locator: string; + workspaceFolderCount?: number; /** - * The number of the interpreters returned by locator + * Time taken to discover using native locator. + */ + nativeDuration?: number; + /** + * The number of the interpreters discovered */ interpreters?: number; + /** + * The number of the interpreters with duplicate prefixes + */ + envsWithDuplicatePrefixes?: number; + /** + * The number of the interpreters returned by `conda info` + */ + condaInfoEnvs?: number; + /** + * The number of the envs_dirs returned by `conda info` + */ + condaInfoEnvsDirs?: number; + /** + * The number of the envs_dirs returned by native locator. + */ + nativeCondaInfoEnvsDirs?: number; + /** + * The number of the conda rc files found using conda info + */ + condaRcs?: number; + /** + * The number of the conda rc files found using native locator. + */ + nativeCondaRcs?: number; + /** + * The number of the conda rc files returned by `conda info` that weren't found by native locator. + */ + nativeCondaRcsNotFound?: number; + /** + * The number of the conda env_dirs returned by `conda info` that weren't found by native locator. + */ + nativeCondaEnvDirsNotFound?: number; + /** + * The number of envs in the env_dirs contained in the count for `nativeCondaEnvDirsNotFound` + */ + nativeCondaEnvDirsNotFoundHasEnvs?: number; + /** + * The number of envs from environments.txt that are in the env_dirs contained in the count for `nativeCondaEnvDirsNotFound` + */ + nativeCondaEnvDirsNotFoundHasEnvsInTxt?: number; + /** + * The number of conda interpreters that are in the one of the global conda env locations. + * Global conda envs locations are returned by `conda info` in the `envs_dirs` setting. + */ + condaEnvsInEnvDir?: number; + /** + * The number of native conda interpreters that are in the one of the global conda env locations. + * Global conda envs locations are returned by `conda info` in the `envs_dirs` setting. + */ + nativeCondaEnvsInEnvDir?: number; + condaRootPrefixEnvsAfterFind?: number; + condaDefaultPrefixEnvsAfterFind?: number; + /** + * A conda env found that matches the root_prefix returned by `conda info` + * However a corresponding conda env not found by native locator. + */ + condaDefaultPrefixFoundInInfoAfterFind?: boolean; + condaRootPrefixFoundInTxt?: boolean; + condaDefaultPrefixFoundInTxt?: boolean; + condaDefaultPrefixFoundInInfoAfterFindKind?: string; + condaRootPrefixFoundAsAnotherKind?: string; + condaRootPrefixFoundAsPrefixOfAnother?: string; + condaDefaultPrefixFoundAsAnotherKind?: string; + condaDefaultPrefixFoundAsPrefixOfAnother?: string; + /** + * Whether we were able to identify the conda root prefix in the conda exe path as a conda env using `find` in native finder API. + */ + condaRootPrefixFoundInInfoAfterFind?: boolean; + /** + * Type of python env detected for the conda root prefix. + */ + condaRootPrefixFoundInInfoAfterFindKind?: string; + /** + * The conda root prefix is found in the conda exe path. + */ + condaRootPrefixInCondaExePath?: boolean; + /** + * A conda env found that matches the root_prefix returned by `conda info` + * However a corresponding conda env not found by native locator. + */ + condaDefaultPrefixFoundInInfoNotInNative?: boolean; + /** + * The conda root prefix is found in the conda exe path. + */ + condaDefaultPrefixInCondaExePath?: boolean; + /** + * User provided a path to the conda exe + */ + userProvidedCondaExe?: boolean; + /** + * The number of conda interpreters without the `conda-meta` directory. + */ + invalidCondaEnvs?: number; + /** + * The number of conda interpreters that have prefix that doesn't exist on disc. + */ + prefixNotExistsCondaEnvs?: number; + /** + * The number of conda interpreters without the prefix. + */ + condaEnvsWithoutPrefix?: number; + /** + * Conda exe can be spawned. + */ + canSpawnConda?: boolean; + /** + * Conda exe can be spawned by native locator. + */ + nativeCanSpawnConda?: boolean; + /** + * Conda env belonging to the conda exe provided by the user is found by native locator. + * I.e. even if the user didn't provide the path to the conda exe, the conda env is found by native locator. + */ + userProvidedEnvFound?: boolean; + /** + * The number of the interpreters not found in disc. + */ + envsNotFount?: number; + /** + * Whether or not we're using the native locator. + */ + usingNativeLocator?: boolean; + /** + * The number of environments discovered not containing an interpreter + */ + environmentsWithoutPython?: number; + /** + * Number of environments of a specific type + */ + activeStateEnvs?: number; + /** + * Number of environments of a specific type + */ + condaEnvs?: number; + /** + * Number of environments of a specific type + */ + customEnvs?: number; + /** + * Number of environments of a specific type + */ + hatchEnvs?: number; + /** + * Number of environments of a specific type + */ + microsoftStoreEnvs?: number; + /** + * Number of environments of a specific type + */ + otherGlobalEnvs?: number; + /** + * Number of environments of a specific type + */ + otherVirtualEnvs?: number; + /** + * Number of environments of a specific type + */ + pipEnvEnvs?: number; + /** + * Number of environments of a specific type + */ + poetryEnvs?: number; + /** + * Number of environments of a specific type + */ + pyenvEnvs?: number; + /** + * Number of environments of a specific type + */ + systemEnvs?: number; + /** + * Number of environments of a specific type + */ + unknownEnvs?: number; + /** + * Number of environments of a specific type + */ + venvEnvs?: number; + /** + * Number of environments of a specific type + */ + virtualEnvEnvs?: number; + /** + * Number of environments of a specific type + */ + virtualEnvWrapperEnvs?: number; + /** + * Number of all known Globals (System, Custom, GlobalCustom, etc) + */ + global?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeEnvironmentsWithoutPython?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeCondaEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeCustomEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeMicrosoftStoreEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeOtherGlobalEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeOtherVirtualEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativePipEnvEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativePoetryEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativePyenvEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeSystemEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeUnknownEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeVenvEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeVirtualEnvEnvs?: number; + /** + * Number of environments of a specific type found by native finder + */ + nativeVirtualEnvWrapperEnvs?: number; + /** + * Number of all known Globals (System, Custom, GlobalCustom, etc) + */ + nativeGlobal?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeCondaEnvs?: number; + /** + * Whether the env txt found by native locator is the same as that found by pythonn ext. + */ + nativeCondaEnvTxtSame?: boolean; + /** + * Number of environments found from env txt by native locator. + */ + nativeCondaEnvsFromTxt?: number; + /** + * Whether the env txt found by native locator exists. + */ + nativeCondaEnvTxtExists?: boolean; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeCustomEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeMicrosoftStoreEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeGlobalEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeOtherVirtualEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativePipEnvEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativePoetryEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativePyenvEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeSystemEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeUnknownEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeVenvEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeVirtualEnvEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeVirtualEnvWrapperEnvs?: number; + /** + * Number of environments of a specific type missing in Native Locator (compared to the Stable Locator). + */ + missingNativeOtherGlobalEnvs?: number; }; /** - * Telemetry event sent when pipenv interpreter discovery is executed. + * Telemetry event sent when Native finder fails to find some conda envs. */ - [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?' + /* __GDPR__ + "native_finder_missing_conda_envs" : { + "missing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "envDirsNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "userProvidedCondaExe" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "rootPrefixNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaPrefixNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "condaManagerNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "missingEnvDirsFromSysRc" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingEnvDirsFromUserRc" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingEnvDirsFromOtherRc" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingFromSysRcEnvDirs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingFromUserRcEnvDirs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingFromOtherRcEnvDirs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" } + } */ - [EventName.CONDA_INHERIT_ENV_PROMPT]: { + [EventName.NATIVE_FINDER_MISSING_CONDA_ENVS]: { /** - * `Yes` When 'Yes' option is selected - * `No` When 'No' option is selected - * `More info` When 'More Info' option is selected + * Number of missing conda environments. */ - 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]: { + missing: number; /** - * `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 + * Total number of env_dirs not found even after parsing the conda_rc files. + * This will tell us that we are either unable to parse some of the conda_rc files or there are other + * env_dirs that we are not able to find. */ - 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]: { + envDirsNotFound?: number; /** - * `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)} + * Whether a conda exe was provided by the user. */ - 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]: { + userProvidedCondaExe?: boolean; /** - * `Download` When the 'Download' option is clicked - * `Ignore` When the prompt is dismissed - * - * @type {('Download' | 'Ignore' | undefined)} + * Whether the user provided a conda executable. */ - 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]: { + rootPrefixNotFound?: boolean; /** - * `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 + * Whether the conda prefix returned by conda was not found by us. */ - 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]: { + condaPrefixNotFound?: boolean; /** - * `Reload` When 'Reload' option is clicked - * `undefined` When prompt is closed - * - * @type {('Reload' | undefined)} + * Whether we found a conda manager or not. */ - selection: 'Reload' | undefined; - }; - /** - * Telemetry sent with details about the current selection of language server - */ - [EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION]: { + condaManagerNotFound?: boolean; + /** + * Whether we failed to find the system rc path. + */ + sysRcNotFound?: boolean; + /** + * Whether we failed to find the user rc path. + */ + userRcNotFound?: boolean; /** - * The startup value of the language server setting + * Number of config files (excluding sys and user rc) that were not found. */ - lsStartup?: LanguageServerType; + otherRcNotFound?: boolean; /** - * Used to track switch between language servers. Carries the final state after the switch. + * Number of conda envs that were not found by us, and the envs belong to env_dirs in the sys config rc. */ - switchTo?: LanguageServerType; + missingEnvDirsFromSysRc?: number; + /** + * Number of conda envs that were not found by us, and the envs belong to env_dirs in the user config rc. + */ + missingEnvDirsFromUserRc?: number; + /** + * Number of conda envs that were not found by us, and the envs belong to env_dirs in the other config rc. + */ + missingEnvDirsFromOtherRc?: number; + /** + * Number of conda envs that were not found by us, and the envs belong to env_dirs in the sys config rc. + */ + missingFromSysRcEnvDirs?: number; + /** + * Number of conda envs that were not found by us, and the envs belong to env_dirs in the user config rc. + */ + missingFromUserRcEnvDirs?: number; + /** + * Number of conda envs that were not found by us, and the envs belong to env_dirs in the other config rc. + */ + missingFromOtherRcEnvDirs?: number; }; /** - * Telemetry event sent with details after attempting to download LS + * Telemetry event sent when Native finder fails to find some conda envs. */ - [EventName.PYTHON_LANGUAGE_SERVER_DOWNLOADED]: { + /* __GDPR__ + "native_finder_missing_poetry_envs" : { + "missing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "missingInPath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "userProvidedPoetryExe" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "poetryExeNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "globalConfigNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "cacheDirNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "cacheDirIsDifferent" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "virtualenvsPathNotFound" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "virtualenvsPathIsDifferent" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "inProjectIsDifferent" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } + } + */ + [EventName.NATIVE_FINDER_MISSING_POETRY_ENVS]: { /** - * Whether LS downloading succeeds + * Number of missing poetry environments. */ - success: boolean; + missing: number; /** - * Version of LS downloaded + * Total number of missing envs, where the envs are created in the virtualenvs_path directory. */ - lsVersion?: string; + missingInPath: number; /** - * Whether download uri starts with `https:` or not + * Whether a poetry exe was provided by the user. */ - usedSSL?: boolean; - + userProvidedPoetryExe?: boolean; + /** + * Whether poetry exe was not found. + */ + poetryExeNotFound?: boolean; /** - * Name of LS downloaded + * Whether poetry config was not found. */ - lsName?: string; + globalConfigNotFound?: boolean; + /** + * Whether cache_dir was not found. + */ + cacheDirNotFound?: boolean; + /** + * Whether cache_dir found was different from that returned by poetry exe. + */ + cacheDirIsDifferent?: boolean; + /** + * Whether virtualenvs.path was not found. + */ + virtualenvsPathNotFound?: boolean; + /** + * Whether virtualenvs.path found was different from that returned by poetry exe. + */ + virtualenvsPathIsDifferent?: boolean; + /** + * Whether virtualenvs.in-project found was different from that returned by poetry exe. + */ + inProjectIsDifferent?: boolean; }; /** - * Telemetry event sent when LS is started for workspace (workspace folder in case of multi-root) + * Telemetry containing performance metrics for Native Finder. */ - [EventName.PYTHON_LANGUAGE_SERVER_ENABLED]: { - lsVersion?: string; - }; - /** - * Telemetry event sent with details when downloading or extracting LS fails + /* __GDPR__ + "native_finder_perf" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "totalDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "breakdownLocators" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "breakdownPath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "breakdownGlobalVirtualEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "breakdownWorkspaces" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorConda" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorHomebrew" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorLinuxGlobalPython" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorMacCmdLineTools" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorMacPythonOrg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorMacXCode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorPipEnv" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorPoetry" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorPixi" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorPyEnv" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorVenv" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorVirtualEnv" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorVirtualEnvWrapper" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorWindowsRegistry" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "locatorWindowsStore" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "timeToSpawn" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "timeToConfigure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "timeToRefresh" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" } + } */ - [EventName.PYTHON_LANGUAGE_SERVER_ERROR]: { + [EventName.NATIVE_FINDER_PERF]: { + /** + * Total duration to find envs using native locator. + * This is the time from the perspective of the Native Locator. + * I.e. starting from the time the request to refresh was received until the end of the refresh. + */ + totalDuration: number; + /** + * Time taken by all locators to find the environments. + * I.e. time for Conda + Poetry + Pyenv, etc (note: all of them run in parallel). + */ + breakdownLocators?: number; + /** + * Time taken to find Python environments in the paths found in the PATH env variable. + */ + breakdownPath?: number; + /** + * Time taken to find Python environments in the global virtual env locations. + */ + breakdownGlobalVirtualEnvs?: number; + /** + * Time taken to find Python environments in the workspaces. + */ + breakdownWorkspaces?: number; + /** + * Time taken to find all global Conda environments. + */ + locatorConda?: number; + /** + * Time taken to find all Homebrew environments. + */ + locatorHomebrew?: number; + /** + * Time taken to find all global Python environments on Linux. + */ + locatorLinuxGlobalPython?: number; + /** + * Time taken to find all Python environments belonging to Mac Command Line Tools . + */ + locatorMacCmdLineTools?: number; + /** + * Time taken to find all Python environments belonging to Mac Python Org. + */ + locatorMacPythonOrg?: number; + /** + * Time taken to find all Python environments belonging to Mac XCode. + */ + locatorMacXCode?: number; + /** + * Time taken to find all Pipenv environments. + */ + locatorPipEnv?: number; + /** + * Time taken to find all Pixi environments. + */ + locatorPixi?: number; + /** + * Time taken to find all Poetry environments. + */ + locatorPoetry?: number; /** - * The error associated with initializing language server + * Time taken to find all Pyenv environments. */ - error: string; + locatorPyEnv?: number; + /** + * Time taken to find all Venv environments. + */ + locatorVenv?: number; + /** + * Time taken to find all VirtualEnv environments. + */ + locatorVirtualEnv?: number; + /** + * Time taken to find all VirtualEnvWrapper environments. + */ + locatorVirtualEnvWrapper?: number; + /** + * Time taken to find all Windows Registry environments. + */ + locatorWindowsRegistry?: number; + /** + * Time taken to find all Windows Store environments. + */ + locatorWindowsStore?: number; + /** + * Total time taken to spawn the Native Python finder process. + */ + timeToSpawn?: number; + /** + * Total time taken to configure the Native Python finder process. + */ + timeToConfigure?: number; + /** + * Total time taken to refresh the Environments (from perspective of Python extension). + * Time = total time taken to process the `refresh` request. + */ + timeToRefresh?: number; }; /** - * Telemetry event sent with details after attempting to extract LS + * Telemetry event sent when discovery of all python environments using the native locator(virtualenv, conda, pipenv etc.) finishes. + */ + /* __GDPR__ + "python_interpreter_discovery_invalid_native" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsCondaEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsCustomEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsMicrosoftStoreEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsOtherVirtualEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsPipEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsPoetryEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsPyenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsSystemEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsUnknownEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsVenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsVirtualEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsVirtualEnvWrapperEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidVersionsOtherGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixCondaEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixCustomEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixMicrosoftStoreEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixOtherVirtualEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixPipEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixPoetryEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixPyenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixSystemEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixUnknownEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixVenvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixVirtualEnvEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixVirtualEnvWrapperEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "invalidSysPrefixOtherGlobalEnvs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" } + } */ - [EventName.PYTHON_LANGUAGE_SERVER_EXTRACTED]: { + [EventName.PYTHON_INTERPRETER_DISCOVERY_INVALID_NATIVE]: { /** - * Whether LS extracting succeeds + * Number of Python envs of a particular type that have invalid version from Native Locator. */ - success: boolean; + invalidVersionsCondaEnvs?: number; /** - * Version of LS extracted + * Number of Python envs of a particular type that have invalid version from Native Locator. */ - lsVersion?: string; + invalidVersionsCustomEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsMicrosoftStoreEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsGlobalEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsOtherVirtualEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsPipEnvEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsPoetryEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsPyenvEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsSystemEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsUnknownEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsVenvEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsVirtualEnvEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsVirtualEnvWrapperEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsOtherGlobalEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixCondaEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixCustomEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixMicrosoftStoreEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixGlobalEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixOtherVirtualEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixPipEnvEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixPoetryEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixPyenvEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixSystemEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixUnknownEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixVenvEnvs?: number; /** - * Whether download uri starts with `https:` or not + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. */ - usedSSL?: boolean; + invalidSysPrefixVirtualEnvEnvs?: number; /** - * Package name of LS extracted + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. */ - lsName?: string; + invalidSysPrefixVirtualEnvWrapperEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixOtherGlobalEnvs?: number; }; /** - * Telemetry event sent if azure blob packages are being listed + * Telemetry event sent with details when user clicks the prompt with the following 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.PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES]: never | undefined; + /* __GDPR__ + "conda_inherit_env_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.CONDA_INHERIT_ENV_PROMPT]: { + /** + * `Yes` When 'Allow' option is selected + * `Close` When 'Close' option is selected + */ + selection: 'Allow' | 'Close' | undefined; + }; + /** - * Tracks if LS is supported on platform or not + * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. + */ + /* __GDPR__ + "require_jupyter_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } */ - [EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED]: { + [EventName.REQUIRE_JUPYTER_PROMPT]: { /** - * Carries `true` if LS is supported, `false` otherwise - * - * @type {boolean} + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `undefined` When 'x' is selected */ - supported: boolean; + selection: 'Yes' | 'No' | undefined; + }; + /** + * Telemetry event sent with details when user clicks the prompt with the following message: + * + * 'We noticed VS Code was launched from an activated conda environment, would you like to select it?' + */ + /* __GDPR__ + "activated_conda_env_launch" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.ACTIVATED_CONDA_ENV_LAUNCH]: { /** - * If checking support for LS failed - * - * @type {'UnknownError'} + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected */ - failureType?: 'UnknownError'; + selection: 'Yes' | 'No' | undefined; }; /** - * Telemetry event sent when LS is ready to start + * 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_LANGUAGE_SERVER_READY]: { - lsVersion?: string; + /* __GDPR__ + "python_interpreter_activate_environment_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `Ignore` When "Don't show again" option is clicked + * + * @type {('Yes' | 'No' | 'Ignore' | undefined)} + */ + selection: 'Yes' | 'No' | 'Ignore' | undefined; }; /** - * Telemetry event sent when starting LS + * 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_LANGUAGE_SERVER_STARTUP]: { - lsVersion?: string; + /* __GDPR__ + "python_not_installed_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [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 sent from language server (details of telemetry sent can be provided by LS team) + * Telemetry event sent when the experiments service is initialized for the first time. + */ + /* __GDPR__ + "python_experiments_init_performance" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } + } + */ + [EventName.PYTHON_EXPERIMENTS_INIT_PERFORMANCE]: unknown; + /** + * Telemetry event sent when the user use the report issue command. + */ + /* __GDPR__ + "use_report_issue_command" : { "owner": "paulacamargo25" } */ - [EventName.PYTHON_LANGUAGE_SERVER_TELEMETRY]: any; + [EventName.USE_REPORT_ISSUE_COMMAND]: unknown; /** - * Telemetry sent when the client makes a request to the language server + * Telemetry event sent when the New Python File command is executed. */ - [EventName.PYTHON_LANGUAGE_SERVER_REQUEST]: any; - /** - * Telemetry event sent with details when inExperiment() API is called + /* __GDPR__ + "create_new_file_command" : { "owner": "luabud" } */ - [EventName.PYTHON_EXPERIMENTS]: { - /** - * Name of the experiment group the user is in - * @type {string} - */ - expName?: string; - }; + [EventName.CREATE_NEW_FILE_COMMAND]: unknown; /** - * Telemetry event sent when Experiments have been disabled. + * Telemetry event sent when the installed versions of Python, Jupyter, and Pylance are all capable + * of supporting the LSP notebooks experiment. This does not indicate that the experiment is enabled. */ - [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 + + /* __GDPR__ + "python_experiments_lsp_notebooks" : { "owner": "luabud" } */ - [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; - }; + [EventName.PYTHON_EXPERIMENTS_LSP_NOTEBOOKS]: unknown; /** - * Telemetry event sent with details when doing best effort to download the experiments within timeout and using it in the current session only + * Telemetry event sent once on session start with details on which experiments are opted into and opted out from. */ - [EventName.PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE]: { + /* __GDPR__ + "python_experiments_opt_in_opt_out_settings" : { + "optedinto" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, + "optedoutfrom" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } + } + */ + [EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS]: { /** - * Carries `true` if downloading experiments successfully finishes within timeout, `false` otherwise - * @type {boolean} + * List of valid experiments in the python.experiments.optInto setting + * @type {string} */ - success?: boolean; + optedInto: string; /** - * Carries an error string if downloading experiments fails with error + * List of valid experiments in the python.experiments.optOutFrom setting * @type {string} */ - error?: string; + optedOutFrom: string; }; /** * Telemetry event sent when LS is started for workspace (workspace folder in case of multi-root) */ + /* __GDPR__ + "language_server_enabled" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.LANGUAGE_SERVER_ENABLED]: { lsVersion?: string; }; /** * Telemetry event sent when Node.js server is ready to start */ + /* __GDPR__ + "language_server_ready" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.LANGUAGE_SERVER_READY]: { lsVersion?: string; }; + /** + * Track how long it takes to trigger language server activation code, after Python extension starts activating. + */ + /* __GDPR__ + "language_server_trigger_time" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, + "triggerTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" } + } + */ + [EventName.LANGUAGE_SERVER_TRIGGER_TIME]: { + /** + * Time it took to trigger language server startup. + */ + triggerTime: number; + }; /** * Telemetry event sent when starting Node.js server */ + /* __GDPR__ + "language_server_startup" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [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; + /* __GDPR__ + "language_server_telemetry" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.LANGUAGE_SERVER_TELEMETRY]: unknown; /** * Telemetry sent when the client makes a request to the Node.js server + * + * This event also has a measure, "resultLength", which records the number of completions provided. */ - [EventName.LANGUAGE_SERVER_REQUEST]: any; + /* __GDPR__ + "language_server_request" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.LANGUAGE_SERVER_REQUEST]: unknown; /** - * Telemetry captured for enabling reload. + * Telemetry send when Language Server is restarted. */ - [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; + /* __GDPR__ + "language_server_restart" : { + "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.LANGUAGE_SERVER_RESTART]: { + reason: 'command' | 'settings' | 'notebooksExperiment'; }; /** - * When user clicks a button in the python extension survey prompt, this telemetry event is sent with details + * Telemetry event sent when Jedi Language Server is started for workspace (workspace folder in case of multi-root) */ - [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; + /* __GDPR__ + "jedi_language_server.enabled" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.JEDI_LANGUAGE_SERVER_ENABLED]: { + lsVersion?: string; }; /** - * Telemetry event sent when 'Extract Method' command is invoked + * Telemetry event sent when Jedi Language Server server is ready to receive messages */ - [EventName.REFACTOR_EXTRACT_FUNCTION]: never | undefined; - /** - * Telemetry event sent when 'Extract Variable' command is invoked + /* __GDPR__ + "jedi_language_server.ready" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } */ - [EventName.REFACTOR_EXTRACT_VAR]: never | undefined; + [EventName.JEDI_LANGUAGE_SERVER_READY]: { + lsVersion?: string; + }; /** - * Telemetry event sent when providing an edit that describes changes to rename a symbol to a different name + * Telemetry event sent when starting Node.js server */ - [EventName.REFACTOR_RENAME]: never | undefined; - /** - * Telemetry event sent when providing a set of project-wide references for the given position and document + /* __GDPR__ + "jedi_language_server.startup" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } */ - [EventName.REFERENCE]: never | undefined; + [EventName.JEDI_LANGUAGE_SERVER_STARTUP]: { + lsVersion?: string; + }; /** - * Telemetry event sent when starting REPL + * Telemetry sent when the client makes a request to the Node.js server + * + * This event also has a measure, "resultLength", which records the number of completions provided. + */ + /* __GDPR__ + "jedi_language_server.request" : { + "method": {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig"} + } */ - [EventName.REPL]: never | undefined; + [EventName.JEDI_LANGUAGE_SERVER_REQUEST]: unknown; /** - * Telemetry event sent with details of linter selected in quickpick of linter list. + * When user clicks a button in the python extension survey prompt, this telemetry event is sent with details */ - [EventName.SELECT_LINTER]: { - /** - * The name of the linter - */ - tool?: LinterId; + /* __GDPR__ + "extension_survey_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.EXTENSION_SURVEY_PROMPT]: { /** - * Carries `true` if linter is enabled, `false` otherwise + * Carries the selection of user when they are asked to take the extension survey */ - enabled: boolean; + selection: 'Yes' | 'Maybe later' | "Don't show again" | undefined; }; /** - * 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?' + * Telemetry event sent when starting REPL */ - [EventName.CONFIGURE_AVAILABLE_LINTER_PROMPT]: { - /** - * Name of the linter tool - * - * @type {LinterId} - */ - tool: LinterId; + /* __GDPR__ + "repl" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "anthonykim1" }, + "repltype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "anthonykim1" } + } + */ + [EventName.REPL]: { /** - * `enable` When 'Enable [linter name]' option is clicked - * `ignore` When 'Not now' option is clicked - * `disablePrompt` When 'Do not show again` option is clicked + * Whether the user launched the Terminal REPL or Native REPL * - * @type {('enable' | 'ignore' | 'disablePrompt' | undefined)} + * Terminal - Terminal REPL user ran `Python: Start Terminal REPL` command. + * Native - Native REPL user ran `Python: Start Native Python REPL` command. + * manualTerminal - User started REPL in terminal using `python`, `python3` or `py` etc without arguments in terminal. + * runningScript - User ran a script in terminal like `python myscript.py`. */ - action: 'enable' | 'ignore' | 'disablePrompt' | undefined; + replType: 'Terminal' | 'Native' | 'manualTerminal' | `runningScript`; }; /** - * Telemetry event sent when providing help for the signature at the given position and document. + * Telemetry event sent when invoking a Tool */ - [EventName.SIGNATURE]: never | undefined; - /** - * Telemetry event sent when providing document symbol information for Jedi autocomplete intellisense + /* __GDPR__ + "invokeTool" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "toolName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "failed": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Whether there was a failure. Common to most of the events.", "owner": "donjayamanne" }, + "failureCategory": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"A reason that we generate (e.g. kerneldied, noipykernel, etc), more like a category of the error. Common to most of the events.", "owner": "donjayamanne" } + } */ - [EventName.SYMBOL]: never | undefined; + [EventName.INVOKE_TOOL]: { + /** + * Tool name. + */ + toolName: string; + /** + * Whether there was a failure. + * Common to most of the events. + */ + failed: boolean; + /** + * A reason the error was thrown. + */ + failureCategory?: string; + }; /** * 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.) */ + /* __GDPR__ + "unittest.configure" : { "owner": "eleanorjboyd" } + */ [EventName.UNITTEST_CONFIGURE]: never | undefined; /** * Telemetry event sent when user chooses a test framework in the Quickpick displayed for enabling and configuring test framework */ + /* __GDPR__ + "unittest.configuring" : { + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "eleanorjboyd" } + } + */ [EventName.UNITTEST_CONFIGURING]: { /** * Name of the test framework to configure @@ -1478,6 +2049,11 @@ export interface IEventNamePropertyMapping { * Telemetry event sent when the extension is activated, if an active terminal is present and * the `python.terminal.activateEnvInCurrentTerminal` setting is set to `true`. */ + /* __GDPR__ + "activate_env_in_current_terminal" : { + "isterminalvisible" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL]: { /** * Carries boolean `true` if an active terminal is present (terminal is visible), `false` otherwise @@ -1487,6 +2063,14 @@ export interface IEventNamePropertyMapping { /** * Telemetry event sent with details when a terminal is created */ + /* __GDPR__ + "terminal.create" : { + "terminal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "triggeredby" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "pythonversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "interpretertype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.TERMINAL_CREATE]: { /** * The type of terminal shell created: powershell, cmd, zsh, bash etc. @@ -1509,355 +2093,117 @@ export interface IEventNamePropertyMapping { /** * The Python interpreter type: Conda, Virtualenv, Venv, Pipenv etc. * - * @type {InterpreterType} + * @type {EnvironmentType} */ - interpreterType?: InterpreterType; + interpreterType?: EnvironmentType; }; /** - * Telemetry event sent with details about discovering tests + * Telemetry event sent indicating the trigger source for discovery. */ - [EventName.UNITTEST_DISCOVER]: { - /** - * The test framework used to discover tests - * - * @type {TestTool} - */ - tool: TestTool; + /* __GDPR__ + "unittest.discovery.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventName.UNITTEST_DISCOVERY_TRIGGER]: { /** * Carries the source which triggered discovering of tests * - * @type {('ui' | 'commandpalette')} - */ - trigger: 'ui' | 'commandpalette'; - /** - * Carries `true` if discovering tests failed, `false` otherwise - * - * @type {boolean} + * @type {('auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter')} + * auto : Triggered by VS Code editor. + * ui : Triggered by clicking a button. + * commandpalette : Triggered by running the command from the command palette. + * watching : Triggered by filesystem or content changes. + * interpreter : Triggered by interpreter change. */ - failed: boolean; + trigger: 'auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter'; }; /** - * Telemetry event is sent if we are doing test discovery using python code + * Telemetry event sent with details about discovering tests */ - [EventName.UNITTEST_DISCOVER_WITH_PYCODE]: never | undefined; - /** - * Telemetry event sent when user clicks a file, function, or suite in test explorer. + /* __GDPR__ + "unittest.discovering" : { + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } */ - [EventName.UNITTEST_NAVIGATE]: { - /** - * Carries `true` if user clicks a file, `false` otherwise - * - * @type {boolean} - */ - byFile?: boolean; + [EventName.UNITTEST_DISCOVERING]: { /** - * Carries `true` if user clicks a function, `false` otherwise + * The test framework used to discover tests * - * @type {boolean} + * @type {TestTool} */ - byFunction?: boolean; + tool: TestTool; + }; + /** + * Telemetry event sent with details about discovering tests + */ + /* __GDPR__ + "unittest.discovery.done" : { + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "failed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventName.UNITTEST_DISCOVERY_DONE]: { /** - * Carries `true` if user clicks a suite, `false` otherwise + * The test framework used to discover tests * - * @type {boolean} + * @type {TestTool} */ - bySuite?: boolean; + tool: TestTool; /** - * Carries `true` if we are changing focus to the suite/file/function, `false` otherwise + * Carries `true` if discovering tests failed, `false` otherwise * * @type {boolean} */ - focus_code?: boolean; + failed: boolean; }; /** - * Tracks number of workspace folders shown in test explorer + * Telemetry event sent when cancelling discovering tests */ - [EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT]: { count: number }; + /* __GDPR__ + "unittest.discovery.stop" : { "owner": "eleanorjboyd" } + */ + [EventName.UNITTEST_DISCOVERING_STOP]: never | undefined; /** * Telemetry event sent with details about running the tests, what is being run, what framework is being used etc. */ + /* __GDPR__ + "unittest.run" : { + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "debugging" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ [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 + * Telemetry event sent when cancelling running tests */ - [EventName.UNITTEST_STOP]: never | undefined; - /** - * Telemetry event sent when disabling all test frameworks + /* __GDPR__ + "unittest.run.stop" : { "owner": "eleanorjboyd" } */ - [EventName.UNITTEST_DISABLE]: never | undefined; + [EventName.UNITTEST_RUN_STOP]: never | undefined; /** - * Telemetry event sent when viewing Python test log output + * Telemetry event sent when run all failed test command is triggered */ - [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 + /* __GDPR__ + "unittest.run.all_failed" : { "owner": "eleanorjboyd" } */ - [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; - /** - * 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 + [EventName.UNITTEST_RUN_ALL_FAILED]: never | undefined; + /** + * Telemetry event sent when testing is disabled for a workspace. */ - [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 + /* __GDPR__ + "unittest.disabled" : { "owner": "eleanorjboyd" } */ - [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 }; + [EventName.UNITTEST_DISABLED]: never | undefined; /* 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. @@ -1879,6 +2225,15 @@ export interface IEventNamePropertyMapping { If true, user has a shell in their environment. If false, user does not have a shell in their environment. */ + /* __GDPR__ + "terminal_shell_identification" : { + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "terminalprovided" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "shellidentificationsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "hascustomshell" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "hasshellinenv" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.TERMINAL_SHELL_IDENTIFICATION]: { failed: boolean; terminalProvided: boolean; @@ -1892,6 +2247,12 @@ export interface IEventNamePropertyMapping { * @type {(undefined | never)} * @memberof IEventNamePropertyMapping */ + /* __GDPR__ + "activate_env_to_get_env_vars_failed" : { + "ispossiblycondaenv" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "terminal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ [EventName.ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED]: { /** * Whether the activation commands contain the name `conda`. @@ -1906,341 +2267,225 @@ export interface IEventNamePropertyMapping { */ terminal: TerminalShellType; }; + + // TensorBoard integration events /** - * 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 event sent when the user is prompted to install Python packages that are + * dependencies for launching an integrated TensorBoard session. */ - [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 + /* __GDPR__ + "tensorboard.session_duration" : { "owner": "donjayamanne" } */ - [Telemetry.CompletionTimeFromLS]: undefined | never; + [EventName.TENSORBOARD_INSTALL_PROMPT_SHOWN]: never | undefined; /** - * 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 event sent after the user has clicked on an option in the prompt we display + * asking them if they want to install Python packages for launching an integrated TensorBoard session. + * `selection` is one of 'yes' or 'no'. */ - [Telemetry.CompletionTimeFromJupyter]: undefined | never; - /** - * Telemetry event sent to indicate the language used in a notebook - * - * @type { language: string } - * @memberof IEventNamePropertyMapping + /* __GDPR__ + "tensorboard.install_prompt_selection" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "operationtype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } + } */ - [Telemetry.NotebookLanguage]: { - /** - * Language found in the notebook if a known language. Otherwise 'unknown' - */ - language: string; + [EventName.TENSORBOARD_INSTALL_PROMPT_SELECTION]: { + selection: TensorBoardPromptSelection; + operationType: 'install' | 'upgrade'; }; /** - * Telemetry event sent to indicate 'jupyter kernelspec' is not possible. - * - * @type {(undefined | never)} - * @memberof IEventNamePropertyMapping + * Telemetry event sent when we find an active integrated terminal running tensorboard. */ - [Telemetry.KernelSpecNotFound]: undefined | never; - /** - * Telemetry event sent to indicate registering a kernel with jupyter failed. - * - * @type {(undefined | never)} - * @memberof IEventNamePropertyMapping + /* __GDPR__ + "tensorboard_detected_in_integrated_terminal" : { "owner": "donjayamanne" } */ - [Telemetry.KernelRegisterFailed]: undefined | never; + [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; /** - * Telemetry event sent to every time a kernel enumeration is done - * - * @type {...} - * @memberof IEventNamePropertyMapping + * Telemetry event sent after attempting to install TensorBoard session dependencies. + * Note, this is only sent if install was attempted. It is not sent if the user opted + * not to install, or if all dependencies were already installed. */ - [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'; + /* __GDPR__ + "tensorboard.package_install_result" : { + "wasprofilerpluginattempted" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" }, + "wastensorboardattempted" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" }, + "wasprofilerplugininstalled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" }, + "wastensorboardinstalled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" } + } + */ + + [EventName.TENSORBOARD_PACKAGE_INSTALL_RESULT]: { + wasProfilerPluginAttempted: boolean; + wasTensorBoardAttempted: boolean; + wasProfilerPluginInstalled: boolean; + wasTensorBoardInstalled: boolean; }; /** - * Total time taken to Launch a raw kernel. + * Telemetry event sent when the user's files contain a PyTorch profiler module + * import. Files are checked for matching imports when they are opened or saved. + * Matches cover import statements of the form `import torch.profiler` and + * `from torch import profiler`. */ - [Telemetry.KernelLauncherPerf]: undefined | never; - /** - * Total time taken to find a kernel on disc. + /* __GDPR__ + "tensorboard.torch_profiler_import" : { "owner": "donjayamanne" } */ - [Telemetry.KernelFinderPerf]: undefined | never; + [EventName.TENSORBOARD_TORCH_PROFILER_IMPORT]: never | undefined; + [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; /** - * Telemetry event sent if there's an error installing a jupyter required dependency - * - * @type { product: string } - * @memberof IEventNamePropertyMapping + * Telemetry event sent before creating an environment. */ - [Telemetry.JupyterInstallFailed]: { - /** - * Product being installed (jupyter or ipykernel or other) - */ - product: string; + /* __GDPR__ + "environment.creating" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "pythonVersion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CREATING]: { + environmentType: 'venv' | 'conda' | 'microvenv' | undefined; + pythonVersion: string | undefined; }; /** - * Telemetry event sent when installing a jupyter dependency - * - * @type {product: string} - * @memberof IEventNamePropertyMapping + * Telemetry event sent after creating an environment, but before attempting package installation. */ - [Telemetry.UserInstalledModule]: { product: string }; - /** - * Telemetry event sent to when user customizes the jupyter command line - * @type {(undefined | never)} - * @memberof IEventNamePropertyMapping + /* __GDPR__ + "environment.created" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [Telemetry.JupyterCommandLineNonDefault]: undefined | never; + [EventName.ENVIRONMENT_CREATED]: { + environmentType: 'venv' | 'conda' | 'microvenv'; + reason: 'created' | 'existing'; + }; /** - * Telemetry event sent when a user runs the interactive window with a new file - * @type {(undefined | never)} - * @memberof IEventNamePropertyMapping + * Telemetry event sent if creating an environment failed. */ - [Telemetry.NewFileForInteractiveWindow]: undefined | never; - /** - * Telemetry event sent when a kernel picked crashes on startup - * @type {(undefined | never)} - * @memberof IEventNamePropertyMapping + /* __GDPR__ + "environment.failed" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [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; + [EventName.ENVIRONMENT_FAILED]: { + environmentType: 'venv' | 'conda' | 'microvenv'; + reason: 'noVenv' | 'noPip' | 'noDistUtils' | 'other'; }; - [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 event sent before installing packages. */ - [Telemetry.GatherQualityReport]: { result: 'yes' | 'no' }; - /** - * Telemetry event sent when the ZMQ native binaries do not work. + /* __GDPR__ + "environment.installing_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [Telemetry.ZMQNotSupported]: undefined | never; + [EventName.ENVIRONMENT_INSTALLING_PACKAGES]: { + environmentType: 'venv' | 'conda' | 'microvenv'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade' | 'pipInstall' | 'pipDownload'; + }; /** - * Telemetry event sent when the ZMQ native binaries do work. + * Telemetry event sent after installing packages. */ - [Telemetry.ZMQSupported]: undefined | never; - /** - * Telemetry event sent with name of a Widget that is used. + /* __GDPR__ + "environment.installed_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [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; + [EventName.ENVIRONMENT_INSTALLED_PACKAGES]: { + environmentType: 'venv' | 'conda'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade'; }; /** - * Telemetry event sent with name of a Widget found. + * Telemetry event sent if installing packages failed. */ - [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'; + /* __GDPR__ + "environment.installing_packages_failed" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED]: { + environmentType: 'venv' | 'conda' | 'microvenv'; + using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipDownload' | 'pipInstall'; }; /** - * 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 event sent if create environment button was used to trigger the command. */ - [Telemetry.DiscoverIPyWidgetNamesLocalPerf]: never | undefined; - /** - * Something went wrong in looking for a widget. + /* __GDPR__ + "environment.button" : {"owner": "karthiknadig" } */ - [Telemetry.HashedIPyWidgetScriptDiscoveryError]: never | undefined; + [EventName.ENVIRONMENT_BUTTON]: never | undefined; /** - * Telemetry event sent when an ipywidget module loads. Module name is hashed. + * Telemetry event if user selected to delete the existing environment. */ - [Telemetry.IPyWidgetLoadSuccess]: { moduleHash: string; moduleVersion: string }; - /** - * Telemetry event sent when an ipywidget module fails to load. Module name is hashed. + /* __GDPR__ + "environment.delete" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "status" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [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; + [EventName.ENVIRONMENT_DELETE]: { + environmentType: 'venv' | 'conda'; + status: 'triggered' | 'deleted' | 'failed'; }; /** - * 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 event if user selected to re-use the existing environment. */ - [Telemetry.IPyWidgetLoadDisabled]: { moduleHash: string; moduleVersion: string }; - /** - * Total time taken to discover a widget script on CDN. + /* __GDPR__ + "environment.reuse" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [Telemetry.DiscoverIPyWidgetNamesCDNPerf]: { - // The CDN we were testing. - cdn: string; - // Whether we managed to find the widget on the CDN or not. - exists: boolean; + [EventName.ENVIRONMENT_REUSE]: { + environmentType: 'venv' | 'conda'; }; /** - * Telemetry sent when we prompt user to use a CDN for IPyWidget scripts. - * This is always sent when we display a prompt. + * Telemetry event sent when a check for environment creation conditions is triggered. */ - [Telemetry.IPyWidgetPromptToUseCDN]: never | undefined; - /** - * Telemetry sent when user does somethign with the prompt displsyed to user about using CDN for IPyWidget scripts. + /* __GDPR__ + "environment.check.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [Telemetry.IPyWidgetPromptToUseCDNSelection]: { - selection: 'ok' | 'cancel' | 'dismissed' | 'doNotShowAgain'; + [EventName.ENVIRONMENT_CHECK_TRIGGER]: { + trigger: + | 'run-in-terminal' + | 'debug-in-terminal' + | 'run-selection' + | 'on-workspace-load' + | 'as-command' + | 'debug'; }; /** - * Telemetry event sent to indicate the overhead of syncing the kernel with the UI. + * Telemetry event sent when a check for environment creation condition is computed. + */ + /* __GDPR__ + "environment.check.result" : { + "result" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } */ - [Telemetry.IPyWidgetOverhead]: { - totalOverheadInMs: number; - numberOfMessagesWaitedOn: number; - averageWaitTime: number; - numberOfRegisteredHooks: number; + [EventName.ENVIRONMENT_CHECK_RESULT]: { + result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; }; /** - * Telemetry event sent when the widget render function fails (note, this may not be sufficient to capture all failures). + * Telemetry event sent when `pip install` was called from a global env in a shell where shell inegration is supported. */ - [Telemetry.IPyWidgetRenderFailure]: never | undefined; - /** - * Telemetry event sent when the widget tries to send a kernel message but nothing was listening + /* __GDPR__ + "environment.terminal.global_pip" : { "owner": "karthiknadig" } */ - [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. - }; + [EventName.ENVIRONMENT_TERMINAL_GLOBAL_PIP]: never | undefined; + /* __GDPR__ + "query-expfeature" : { + "owner": "luabud", + "comment": "Logs queries to the experiment service by feature for metric calculations", + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } + } + */ + /* __GDPR__ + "call-tas-error" : { + "owner": "luabud", + "comment": "Logs when calls to the experiment service fails", + "errortype": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Type of error when calling TAS (ServerError, NoResponse, etc.)"} + } + */ } diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts new file mode 100644 index 000000000000..63bd113893e2 --- /dev/null +++ b/src/client/telemetry/pylance.ts @@ -0,0 +1,484 @@ +/* __GDPR__ + "language_server.enabled" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.jinja_usage" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } , + "openfileextensions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.ready" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.request" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "moduleversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.startup" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/analysis_complete" : { + "configparseerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "elapsedms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "externalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "fatalerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "heaptotalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "heapusedmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isdone" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "isfirstrun" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "numfilesanalyzed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "numfilesinprogram" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "diagnosticsseen" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "editablepthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "computedpthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + + } +*/ +/* __GDPR__ + "language_server/analysis_exception" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_accepted" : { + "autoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "dictionarykey" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "memberaccess" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "keyword" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_coverage" : { + "failures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "overallfailures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "overallsuccesses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "overalltotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "successes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_metrics" : { + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lastknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lastknownmodulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "packagehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "correlationid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportadditiontimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportedittimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportimportaliascount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportimportaliastimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindextimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindexused" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportitemcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportmoduleresolvetimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportmoduletimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportsymbolcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimporttotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportuserindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completionitems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completionitemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_extensiontotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completiontype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_filetype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_context_items" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "context" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/documentcolor_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/exception_intellicode" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/execute_command" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/goto_def_inside_string" : { + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/import_heuristic" : { + "avgcost" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "avglevel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "conflicts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_because_it_is_not_a_valid_directory" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_could_not_parse_output" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason_did_not_find_file" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_no_python_interpreter_search_path" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_typeshed_path_not_found" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "success" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/import_metrics" : { + "absolutestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absolutetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absoluteunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absoluteuserunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "builtinimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "builtinimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "localimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "localimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativeunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "stubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "thirdpartyimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "thirdpartyimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedmodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedpackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedpackageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedtotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/index_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/installed_packages" : { + "packagesbitarray" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "packageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "editablepthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/intellicode_completion_item_selected" : { + "class" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "elapsedtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failurereason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isintellicodecommit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "language" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "memoryincreasekb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "methods" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modeltype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "modelversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_enabled" : { + "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "startup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_model_load_failed" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_onnx_load_failed" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/rename_files" : { + "affectedfilescount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "filerenamed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/semantictokens_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/server_side_request" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/settings" : { + "addimportexactmatchonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aicodeactionsimplementabstractclasses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsGenerateDocstring" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsGenerateSymbols" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsConvertFormatString" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "autoimportcompletions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "autosearchpaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "callArgumentNameInlayHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "completefunctionparens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableTaggedHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "enableextractcodeaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "enablePytestSupport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extracommitchars" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasconfigfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasextrapaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "intelliCodeEnabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "includeusersymbolsinautoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "indexing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "languageservermode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "movesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nodeExecutable" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "openfilesonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "pytestparameterinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unusablecompilerflags": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/startup_metrics" : { + "analysisms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "presetfileopenms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokendeltams" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenfullms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenrangems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totalms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "userindexms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/workspaceindex_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/workspaceindex_threshold_reached" : { + "index_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/mcp_tool" : { + "kind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "cancelled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "cancellation_reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/copilot_hover" : { + "symbolName" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/** + * Telemetry event sent when LSP server crashes + */ +/* __GDPR__ +"language_server.crash" : { + "oom" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } +} +*/ diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index cc17128c858b..42e51b261129 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -1,25 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -import { IEventNamePropertyMapping } from '../telemetry/index'; +import type { IEventNamePropertyMapping } from './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 TestTool = 'pytest' | 'unittest'; export type TestRunTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_RUN]; -export type TestDiscoverytTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVER]; +export type TestDiscoveryTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_DONE]; 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/tensorBoard/constants.ts b/src/client/tensorBoard/constants.ts new file mode 100644 index 000000000000..aec38eecd95f --- /dev/null +++ b/src/client/tensorBoard/constants.ts @@ -0,0 +1,25 @@ +export enum TensorBoardPromptSelection { + Yes = 'yes', + No = 'no', + DoNotAskAgain = 'doNotAskAgain', + None = 'none', +} + +export enum TensorBoardEntrypointTrigger { + tfeventfiles = 'tfeventfiles', + fileimport = 'fileimport', + nbextension = 'nbextension', + palette = 'palette', +} + +export enum TensorBoardSessionStartResult { + cancel = 'canceled', + success = 'success', + error = 'error', +} + +export enum TensorBoardEntrypoint { + prompt = 'prompt', + codelens = 'codelens', + palette = 'palette', +} diff --git a/src/client/tensorBoard/helpers.ts b/src/client/tensorBoard/helpers.ts new file mode 100644 index 000000000000..8da3ef6a38f2 --- /dev/null +++ b/src/client/tensorBoard/helpers.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// While it is uncommon for users to `import tensorboard`, TensorBoard is frequently +// included as a submodule of other packages, e.g. torch.utils.tensorboard. +// This is a modified version of the regex from src/client/telemetry/importTracker.ts +// in order to match on imported submodules as well, since the original regex only +// matches the 'main' module. + +// RegEx to match `import torch.profiler` or `from torch import profiler` +export const TorchProfilerImportRegEx = /^\s*(?:import (?:(\w+, )*torch\.profiler(, \w+)*))|(?:from torch import (?:(\w+, )*profiler(, \w+)*))/; diff --git a/src/client/tensorBoard/serviceRegistry.ts b/src/client/tensorBoard/serviceRegistry.ts new file mode 100644 index 000000000000..9f53af72053e --- /dev/null +++ b/src/client/tensorBoard/serviceRegistry.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceManager } from '../ioc/types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; + +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton(TensorBoardPrompt, TensorBoardPrompt); + serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); +} diff --git a/src/client/tensorBoard/tensorBoardPrompt.ts b/src/client/tensorBoard/tensorBoardPrompt.ts new file mode 100644 index 000000000000..563419bd4ea6 --- /dev/null +++ b/src/client/tensorBoard/tensorBoardPrompt.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IPersistentState, IPersistentStateFactory } from '../common/types'; + +enum TensorBoardPromptStateKeys { + ShowNativeTensorBoardPrompt = 'showNativeTensorBoardPrompt', +} + +@injectable() +export class TensorBoardPrompt { + private state: IPersistentState; + + constructor(@inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory) { + this.state = this.persistentStateFactory.createWorkspacePersistentState( + TensorBoardPromptStateKeys.ShowNativeTensorBoardPrompt, + true, + ); + } + + public isPromptEnabled(): boolean { + return this.state.value; + } +} diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts new file mode 100644 index 000000000000..b18202810e45 --- /dev/null +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { createPromiseFromCancellation } from '../common/cancellation'; +import { IInstaller, InstallerResponse, ProductInstallStatus, Product } from '../common/types'; +import { Common, TensorBoard } from '../common/utils/localize'; +import { IInterpreterService } from '../interpreter/contracts'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ImportTracker } from '../telemetry/importTracker'; +import { TensorBoardPromptSelection } from './constants'; +import { ModuleInstallFlags } from '../common/installer/types'; +import { traceError, traceVerbose } from '../logging'; + +const TensorBoardSemVerRequirement = '>= 2.4.1'; +const TorchProfilerSemVerRequirement = '>= 0.2.0'; + +/** + * Manages the lifecycle of a TensorBoard session. + * Specifically, it: + * - ensures the TensorBoard Python package is installed, + * - asks the user for a log directory to start TensorBoard with + * - spawns TensorBoard in a background process which must stay running + * to serve the TensorBoard website + * - frames the TensorBoard website in a VSCode webview + * - shuts down the TensorBoard process when the webview is closed + */ +export class TensorBoardSession { + constructor( + private readonly installer: IInstaller, + private readonly interpreterService: IInterpreterService, + private readonly commandManager: ICommandManager, + private readonly applicationShell: IApplicationShell, + ) {} + + private async promptToInstall( + tensorBoardInstallStatus: ProductInstallStatus, + profilerPluginInstallStatus: ProductInstallStatus, + ) { + sendTelemetryEvent(EventName.TENSORBOARD_INSTALL_PROMPT_SHOWN); + const yes = Common.bannerLabelYes; + const no = Common.bannerLabelNo; + const isUpgrade = tensorBoardInstallStatus === ProductInstallStatus.NeedsUpgrade; + let message; + + if ( + tensorBoardInstallStatus === ProductInstallStatus.Installed && + profilerPluginInstallStatus !== ProductInstallStatus.Installed + ) { + // PyTorch user already has TensorBoard, just ask if they want the profiler plugin + message = TensorBoard.installProfilerPluginPrompt; + } else if (profilerPluginInstallStatus !== ProductInstallStatus.Installed) { + // PyTorch user doesn't have compatible TensorBoard or the profiler plugin + message = TensorBoard.installTensorBoardAndProfilerPluginPrompt; + } else if (isUpgrade) { + // Not a PyTorch user and needs upgrade, don't need to mention profiler plugin + message = TensorBoard.upgradePrompt; + } else { + // Not a PyTorch user and needs install, again don't need to mention profiler plugin + message = TensorBoard.installPrompt; + } + const selection = await this.applicationShell.showErrorMessage(message, ...[yes, no]); + let telemetrySelection = TensorBoardPromptSelection.None; + if (selection === yes) { + telemetrySelection = TensorBoardPromptSelection.Yes; + } else if (selection === no) { + telemetrySelection = TensorBoardPromptSelection.No; + } + sendTelemetryEvent(EventName.TENSORBOARD_INSTALL_PROMPT_SELECTION, undefined, { + selection: telemetrySelection, + operationType: isUpgrade ? 'upgrade' : 'install', + }); + return selection; + } + + // Ensure that the TensorBoard package is installed before we attempt + // to start a TensorBoard session. If the user has a torch import in + // any of their open documents, also try to install the torch-tb-plugin + // package, but don't block if installing that fails. + public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise { + traceVerbose('Ensuring TensorBoard package is installed into active interpreter'); + const interpreter = + (await this.interpreterService.getActiveInterpreter(resource)) || + (await this.commandManager.executeCommand('python.setInterpreter')); + if (!interpreter) { + return false; + } + + // First see what dependencies we're missing + let [tensorboardInstallStatus, profilerPluginInstallStatus] = await Promise.all([ + this.installer.isProductVersionCompatible(Product.tensorboard, TensorBoardSemVerRequirement, interpreter), + this.installer.isProductVersionCompatible( + Product.torchProfilerImportName, + TorchProfilerSemVerRequirement, + interpreter, + ), + ]); + const isTorchUser = ImportTracker.hasModuleImport('torch'); + const needsTensorBoardInstall = tensorboardInstallStatus !== ProductInstallStatus.Installed; + const needsProfilerPluginInstall = profilerPluginInstallStatus !== ProductInstallStatus.Installed; + if ( + // PyTorch user, in profiler install experiment, TensorBoard and profiler plugin already installed + (isTorchUser && !needsTensorBoardInstall && !needsProfilerPluginInstall) || + // Not PyTorch user or not in profiler install experiment, so no need for profiler plugin, + // and TensorBoard is already installed + (!isTorchUser && tensorboardInstallStatus === ProductInstallStatus.Installed) + ) { + return true; + } + + // Ask the user if they want to install packages to start a TensorBoard session + const selection = await this.promptToInstall( + tensorboardInstallStatus, + isTorchUser ? profilerPluginInstallStatus : ProductInstallStatus.Installed, + ); + if (selection !== Common.bannerLabelYes && !needsTensorBoardInstall) { + return true; + } + if (selection !== Common.bannerLabelYes) { + return false; + } + + // User opted to install packages. Figure out which ones we need and install them + const tokenSource = new CancellationTokenSource(); + const installerToken = tokenSource.token; + const cancellationPromise = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: InstallerResponse.Ignore, + token: installerToken, + }); + const installPromises = []; + // If need to install torch.profiler and it's not already installed, add it to our list of promises + if (needsTensorBoardInstall) { + installPromises.push( + this.installer.install( + Product.tensorboard, + interpreter, + installerToken, + tensorboardInstallStatus === ProductInstallStatus.NeedsUpgrade + ? ModuleInstallFlags.upgrade + : undefined, + ), + ); + } + if (isTorchUser && needsProfilerPluginInstall) { + installPromises.push( + this.installer.install( + Product.torchProfilerInstallName, + interpreter, + installerToken, + profilerPluginInstallStatus === ProductInstallStatus.NeedsUpgrade + ? ModuleInstallFlags.upgrade + : undefined, + ), + ); + } + await Promise.race([...installPromises, cancellationPromise]); + + // Check install status again after installing + [tensorboardInstallStatus, profilerPluginInstallStatus] = await Promise.all([ + this.installer.isProductVersionCompatible(Product.tensorboard, TensorBoardSemVerRequirement, interpreter), + this.installer.isProductVersionCompatible( + Product.torchProfilerImportName, + TorchProfilerSemVerRequirement, + interpreter, + ), + ]); + // Send telemetry regarding results of install + sendTelemetryEvent(EventName.TENSORBOARD_PACKAGE_INSTALL_RESULT, undefined, { + wasTensorBoardAttempted: needsTensorBoardInstall, + wasProfilerPluginAttempted: needsProfilerPluginInstall, + wasTensorBoardInstalled: tensorboardInstallStatus === ProductInstallStatus.Installed, + wasProfilerPluginInstalled: profilerPluginInstallStatus === ProductInstallStatus.Installed, + }); + // Profiler plugin is not required to start TensorBoard. If it failed, note that it failed + // in the log, but report success only based on TensorBoard package install status. + if (isTorchUser && profilerPluginInstallStatus !== ProductInstallStatus.Installed) { + traceError(`Failed to install torch-tb-plugin. Profiler plugin will not appear in TensorBoard session.`); + } + return tensorboardInstallStatus === ProductInstallStatus.Installed; + } +} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts new file mode 100644 index 000000000000..995344284eec --- /dev/null +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { IInstaller } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { TensorBoardSession } from './tensorBoardSession'; + +@injectable() +export class TensorboardDependencyChecker { + constructor( + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + ) {} + + public async ensureDependenciesAreInstalled(resource?: Uri): Promise { + const newSession = new TensorBoardSession( + this.installer, + this.interpreterService, + this.commandManager, + this.applicationShell, + ); + const result = await newSession.ensurePrerequisitesAreInstalled(resource); + return result; + } +} diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts new file mode 100644 index 000000000000..f3cbad59977b --- /dev/null +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -0,0 +1,88 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Extension, Uri } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; +import { IExtensions, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; + +type PythonApiForTensorboardExtension = { + /** + * Gets activated env vars for the active Python Environment for the given resource. + */ + getActivatedEnvironmentVariables(resource: Resource): Promise; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ + ensureDependenciesAreInstalled(resource?: Uri): Promise; + /** + * Whether to allow displaying tensorboard prompt. + */ + isPromptEnabled(): boolean; +}; + +type TensorboardExtensionApi = { + /** + * Registers python extension specific parts with the tensorboard extension + */ + registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void; +}; + +@injectable() +export class TensorboardExtensionIntegration { + private tensorboardExtension: Extension | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, + @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, + ) {} + + public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); + return undefined; + } + tensorboardExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async (resource: Resource) => + this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true), + ensureDependenciesAreInstalled: async (resource?: Uri): Promise => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), + }); + return undefined; + } + + public async integrateWithTensorboardExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension(TENSORBOARD_EXTENSION_ID); + if (!extension) { + return undefined; + } + await extension.activate(); + if (extension.isActive) { + this.tensorboardExtension = extension; + return this.tensorboardExtension.exports; + } + } else { + return this.tensorboardExtension.exports; + } + return undefined; + } +} diff --git a/src/client/terminals/activation.ts b/src/client/terminals/activation.ts index b42d4b3bd4b0..ed26916e3eaa 100644 --- a/src/client/terminals/activation.ts +++ b/src/client/terminals/activation.ts @@ -4,71 +4,67 @@ '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 { Terminal, Uri } from 'vscode'; +import { IActiveResourceService, ITerminalManager } from '../common/application/types'; 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 { IDisposable, IDisposableRegistry } from '../common/types'; 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; - } -} +import { shouldEnvExtHandleActivation } from '../envExt/api.internal'; @injectable() export class TerminalAutoActivation implements ITerminalAutoActivation { private handler?: IDisposable; + + private readonly terminalsNotToAutoActivate = new WeakSet(); + constructor( - @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(ITerminalManager) + private readonly terminalManager: ITerminalManager, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(ITerminalActivator) private readonly activator: ITerminalActivator, - @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService + @inject(IActiveResourceService) + private readonly activeResourceService: IActiveResourceService, ) { disposableRegistry.push(this); } - public dispose() { + + public dispose(): void { if (this.handler) { this.handler.dispose(); this.handler = undefined; } } - public register() { + + public register(): void { if (this.handler) { return; } this.handler = this.terminalManager.onDidOpenTerminal(this.activateTerminal, this); } + + public disableAutoActivation(terminal: Terminal): void { + this.terminalsNotToAutoActivate.add(terminal); + } + private async activateTerminal(terminal: Terminal): Promise { + if (this.terminalsNotToAutoActivate.has(terminal)) { + return; + } + if (shouldEnvExtHandleActivation()) { + return; + } 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. + + const cwd = + 'cwd' in terminal.creationOptions + ? terminal.creationOptions.cwd + : this.activeResourceService.getActiveResource(); + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + await this.activator.activateEnvironmentInTerminal(terminal, { - resource: this.activeResourceService.getActiveResource() + resource, }); } } diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 312eef37b180..48165adcd169 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -3,25 +3,26 @@ 'use strict'; -import { inject, injectable, named } from 'inversify'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; - +import { inject, injectable } from 'inversify'; +import { Disposable, EventEmitter, Terminal, Uri } from 'vscode'; +import * as path from 'path'; 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 { IDisposableRegistry, IConfigurationService, Resource } from '../../common/types'; import { noop } from '../../common/utils/misc'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { traceError, traceVerbose } from '../../logging'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../terminals/types'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; +import { ReplType } from '../../repl/types'; +import { runInDedicatedTerminal, runInTerminal, useEnvExtension } from '../../envExt/api.internal'; @injectable() export class CodeExecutionManager implements ICodeExecutionManager { @@ -30,66 +31,155 @@ export class CodeExecutionManager implements ICodeExecutionManager { @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 + @inject(IConfigurationService) private readonly configSettings: IConfigurationService, + @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) => { + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( + (cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + traceVerbose(`Attempting to run Python file`, file?.fsPath); const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger).catch((ex) => - traceError('Failed to execute file in terminal', ex) - ); - } - ) - ); - }); + const newTerminalPerFile = cmd === Commands.Exec_In_Separate_Terminal; + + if (useEnvExtension()) { + try { + await this.executeUsingExtension(file, cmd === Commands.Exec_In_Separate_Terminal); + } catch (ex) { + traceError('Failed to execute file in terminal', ex); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile, + }); + return; + } + + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager + .executeCommand(Commands.TriggerEnvironmentSelection, file) + .then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + + await this.executeFileInTerminal(file, trigger, { + newTerminalPerFile, + }) + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }) + .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.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + await this.executeSelectionInTerminal().then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }); + }), ); this.disposableRegistry.push( this.commandManager.registerCommand( - Commands.Exec_Selection_In_Django_Shell, - this.executeSelectionInDjangoShell.bind(this) - ) + Commands.Exec_Selection_In_Django_Shell as any, + async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + await this.executeSelectionInDjangoShell().then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }); + }, + ), ); } - private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); + + private async executeUsingExtension(file: Resource, dedicated: boolean): Promise { const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; - const fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + let 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 fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; + } + + // Check on setting terminal.executeInFileDir + const pythonSettings = this.configSettings.getSettings(file); + let cwd = pythonSettings.terminal.executeInFileDir ? path.dirname(fileToExecute.fsPath) : undefined; + + // Check on setting terminal.launchArgs + const launchArgs = pythonSettings.terminal.launchArgs; + const totalArgs = [...launchArgs, fileToExecute.fsPath.fileToCommandArgumentForPythonExt()]; + + const show = this.shouldTerminalFocusOnStart(fileToExecute); + let terminal: Terminal | undefined; + if (dedicated) { + terminal = await runInDedicatedTerminal(fileToExecute, totalArgs, cwd, show); + } else { + terminal = await runInTerminal(fileToExecute, totalArgs, cwd, show); + } + + if (terminal) { + terminal.show(); + } + } + + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); + const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); + file = file instanceof Uri ? file : undefined; + let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + if (!fileToExecute) { + return; + } + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) @@ -97,8 +187,6 @@ export class CodeExecutionManager implements ICodeExecutionManager { 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) @@ -113,8 +201,16 @@ export class CodeExecutionManager implements ICodeExecutionManager { return; } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); - const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); + const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor); + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await codeExecutionHelper.normalizeLines( + codeToExecute!, + ReplType.terminal, + wholeFileContent, + ); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } @@ -127,6 +223,10 @@ export class CodeExecutionManager implements ICodeExecutionManager { noop(); } - await executionService.execute(normalizedCode, activeEditor!.document.uri); + await executionService.execute(normalizedCode, activeEditor.document.uri); + } + + private shouldTerminalFocusOnStart(uri: Uri | undefined): boolean { + return this.configSettings.getSettings(uri)?.terminal.focusAfterLaunch; } } diff --git a/src/client/terminals/codeExecution/djangoContext.ts b/src/client/terminals/codeExecution/djangoContext.ts index 2c3de7b79a07..74643084db28 100644 --- a/src/client/terminals/codeExecution/djangoContext.ts +++ b/src/client/terminals/codeExecution/djangoContext.ts @@ -6,8 +6,8 @@ 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'; +import { traceError } from '../../logging'; @injectable() export class DjangoContextInitializer implements Disposable { @@ -21,12 +21,12 @@ export class DjangoContextInitializer implements Disposable { private documentManager: IDocumentManager, private workpaceService: IWorkspaceService, private fileSystem: IFileSystem, - commandManager: ICommandManager + 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()) + this.workpaceService.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace()), ); } diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index 0e96c13e1690..05a1470b5727 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -6,11 +6,17 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Uri } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { + IApplicationShell, + 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 { IInterpreterService } from '../../interpreter/contracts'; import { copyPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; import { DjangoContextInitializer } from './djangoContext'; import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; @@ -25,9 +31,20 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi @inject(IPlatformService) platformService: IPlatformService, @inject(ICommandManager) commandManager: ICommandManager, @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IDisposableRegistry) disposableRegistry: Disposable[] + @inject(IDisposableRegistry) disposableRegistry: Disposable[], + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(IApplicationShell) applicationShell: IApplicationShell, ) { - super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + super( + terminalServiceFactory, + configurationService, + workspace, + disposableRegistry, + platformService, + interpreterService, + commandManager, + applicationShell, + ); this.terminalTitle = 'Django Shell'; disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); } @@ -43,7 +60,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi 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']); + return copyPythonExecInfo(info, [managePyPath.fileToCommandArgumentForPythonExt(), 'shell']); } public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 87af541f7547..4efad5ee174e 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -3,30 +3,62 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; -import { Range, TextEditor, Uri } from 'vscode'; +import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { + IActiveResourceService, + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} 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 { createDeferred } from '../../common/utils/async'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; +import { traceError } from '../../logging'; +import { IConfigurationService, Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ReplType } from '../../repl/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) { + + private readonly commandManager: ICommandManager; + + private activeResourceService: IActiveResourceService; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error TS6133: 'configSettings' is declared but its value is never read. + private readonly configSettings: IConfigurationService; + + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.documentManager = serviceContainer.get(IDocumentManager); this.applicationShell = serviceContainer.get(IApplicationShell); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); this.interpreterService = serviceContainer.get(IInterpreterService); + this.configSettings = serviceContainer.get(IConfigurationService); + this.commandManager = serviceContainer.get(ICommandManager); + this.activeResourceService = this.serviceContainer.get(IActiveResourceService); } - public async normalizeLines(code: string, resource?: Uri): Promise { + + public async normalizeLines( + code: string, + _replType: ReplType, + wholeFileContent?: string, + resource?: Uri, + ): Promise { try { if (code.trim().length === 0) { return ''; @@ -34,57 +66,260 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { // 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 activeEditor = this.documentManager.activeTextEditor; 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); + const [args, parse] = internalScripts.normalizeSelection(); + const observable = processService.execObservable(interpreter?.path || 'python', args, { + throwOnStdErr: true, + }); + const normalizeOutput = createDeferred(); + + // Read result from the normalization script from stdout, and resolve the promise when done. + let normalized = ''; + observable.out.subscribe({ + next: (output) => { + if (output.source === 'stdout') { + normalized += output.out; + } + }, + complete: () => { + normalizeOutput.resolve(normalized); + }, + }); + // If there is no explicit selection, we are exeucting 'line' or 'block'. + if (activeEditor?.selection?.isEmpty) { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'line' }); + } + // The normalization script expects a serialized JSON object, with the selection under the "code" key. + // We're using a JSON object so that we don't have to worry about encoding, or escaping non-ASCII characters. + const startLineVal = activeEditor?.selection?.start.line ?? 0; + const endLineVal = activeEditor?.selection?.end.line ?? 0; + const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; + let smartSendSettingsEnabledVal = true; + let shellIntegrationEnabled = false; + const configuration = this.serviceContainer.get(IConfigurationService); + if (configuration) { + const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); + smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend; + shellIntegrationEnabled = pythonSettings.terminal.shellIntegration.enabled; + } + + const input = JSON.stringify({ + code, + wholeFileContent, + startLine: startLineVal, + endLine: endLineVal, + emptyHighlight: emptyHighlightVal, + smartSendSettingsEnabled: smartSendSettingsEnabledVal, + }); + observable.proc?.stdin?.write(input); + observable.proc?.stdin?.end(); + + // We expect a serialized JSON object back, with the normalized code under the "normalized" key. + const result = await normalizeOutput.promise; + const object = JSON.parse(result); + + if (activeEditor?.selection && smartSendSettingsEnabledVal && object.normalized !== 'deprecated') { + const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; + await this.moveToNextBlock(lineOffset, activeEditor); + } + + // For new _pyrepl for Python3.13+ && !shellIntegration, we need to send code via bracketed paste mode. + if (object.attach_bracket_paste && !shellIntegrationEnabled && _replType === ReplType.terminal) { + let trimmedNormalized = object.normalized.replace(/\n$/, ''); + if (trimmedNormalized.endsWith(':\n')) { + // In case where statement is unfinished via :, truncate so auto-indentation lands nicely. + trimmedNormalized = trimmedNormalized.replace(/\n$/, ''); + } + return `\u001b[200~${trimmedNormalized}\u001b[201~`; + } + + return parse(object.normalized); } catch (ex) { traceError(ex, 'Python: Failed to normalize code for execution in terminal'); return code; } } + /** + * Depending on whether or not user is in experiment for smart send, + * dynamically move the cursor to the next block of code. + * The cursor movement is not moved by one everytime, + * since with the smart selection, the next executable code block + * can be multiple lines away. + * Intended to provide smooth shift+enter user experience + * bringing user's cursor to the next executable block of code when used with smart selection. + */ + // eslint-disable-next-line class-methods-use-this + private async moveToNextBlock(lineOffset: number, activeEditor?: TextEditor): Promise { + if (activeEditor?.selection?.isEmpty) { + await this.commandManager.executeCommand('cursorMove', { + to: 'down', + by: 'line', + value: Number(lineOffset), + }); + await this.commandManager.executeCommand('cursorEnd'); + } + + return Promise.resolve(); + } + public async getFileToExecute(): Promise { - const activeEditor = this.documentManager.activeTextEditor!; + const activeEditor = this.documentManager.activeTextEditor; if (!activeEditor) { - this.applicationShell.showErrorMessage('No open file to run in terminal'); - return; + this.applicationShell.showErrorMessage(l10n.t('No open file to run in terminal')); + return undefined; } if (activeEditor.document.isUntitled) { - this.applicationShell.showErrorMessage('The active file needs to be saved before it can be run'); - return; + this.applicationShell.showErrorMessage(l10n.t('The active file needs to be saved before it can be run')); + return undefined; } if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { - this.applicationShell.showErrorMessage('The active file is not a Python source file'); - return; + this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file')); + return undefined; } if (activeEditor.document.isDirty) { await activeEditor.document.save(); } + return activeEditor.document.uri; } + // eslint-disable-next-line class-methods-use-this public async getSelectedTextToExecute(textEditor: TextEditor): Promise { if (!textEditor) { - return; + return undefined; } - const selection = textEditor.selection; + const { selection } = textEditor; let code: string; + if (selection.isEmpty) { code = textEditor.document.lineAt(selection.start.line).text; + } else if (selection.isSingleLine) { + code = getSingleLineSelectionText(textEditor); } else { - const textRange = new Range(selection.start, selection.end); - code = textEditor.document.getText(textRange); + code = getMultiLineSelectionText(textEditor); } + return code; } - public async saveFileIfDirty(file: Uri): Promise { + + 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(); + if (docs.length === 1 && (docs[0].isDirty || docs[0].isUntitled)) { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + return workspaceService.save(docs[0].uri); } + return undefined; } } + +export function getSingleLineSelectionText(textEditor: TextEditor): string { + const { selection } = textEditor; + const selectionRange = new Range(selection.start, selection.end); + const selectionText = textEditor.document.getText(selectionRange); + const fullLineText = textEditor.document.lineAt(selection.start.line).text; + + if (selectionText.trim() === fullLineText.trim()) { + // This handles the following case: + // if (x): + // print(x) + // ↑------↑ <--- selection range + // + // We should return: + // print(x) + // ↑----------↑ <--- text including the initial white space + return fullLineText; + } + + // This is where part of the line is selected: + // if(isPrime(x) || isFibonacci(x)): + // ↑--------↑ <--- selection range + // + // We should return just the selection: + // isPrime(x) + return selectionText; +} + +export function getMultiLineSelectionText(textEditor: TextEditor): string { + const { selection } = textEditor; + const selectionRange = new Range(selection.start, selection.end); + const selectionText = textEditor.document.getText(selectionRange); + + const fullTextRange = new Range( + new Position(selection.start.line, 0), + new Position(selection.end.line, textEditor.document.lineAt(selection.end.line).text.length), + ); + const fullText = textEditor.document.getText(fullTextRange); + + // This handles case where: + // def calc(m, n): + // ↓<------------------------------- selection start + // print(m) + // print(n) + // ↑<------------------------ selection end + // if (m == 0): + // return n + 1 + // if (m > 0 and n == 0): + // return calc(m - 1 , 1) + // return calc(m - 1, calc(m, n - 1)) + // + // We should return: + // ↓<---------------------------------- From here + // print(m) + // print(n) + // ↑<----------------------- To here + if (selectionText.trim() === fullText.trim()) { + return fullText; + } + + const fullStartLineText = textEditor.document.lineAt(selection.start.line).text; + const selectionFirstLineRange = new Range( + selection.start, + new Position(selection.start.line, fullStartLineText.length), + ); + const selectionFirstLineText = textEditor.document.getText(selectionFirstLineRange); + + // This handles case where: + // def calc(m, n): + // ↓<------------------------------ selection start + // if (m == 0): + // return n + 1 + // ↑<------------------- selection end (notice " + 1" is not selected) + // if (m > 0 and n == 0): + // return calc(m - 1 , 1) + // return calc(m - 1, calc(m, n - 1)) + // + // We should return: + // ↓<---------------------------------- From here + // if (m == 0): + // return n + 1 + // ↑<------------------- To here (notice " + 1" is not selected) + if (selectionFirstLineText.trimLeft() === fullStartLineText.trimLeft()) { + return fullStartLineText + selectionText.substr(selectionFirstLineText.length); + } + + // If you are here then user has selected partial start and partial end lines: + // def calc(m, n): + + // if (m == 0): + // return n + 1 + + // ↓<------------------------------- selection start + // if (m > 0 + // and n == 0): + // ↑<-------------------- selection end + // return calc(m - 1 , 1) + // return calc(m - 1, calc(m, n - 1)) + // + // We should return: + // ↓<---------------------------------- From here + // (m > 0 + // and n == 0) + // ↑<---------------- To here + return selectionText; +} diff --git a/src/client/terminals/codeExecution/repl.ts b/src/client/terminals/codeExecution/repl.ts index f3c620e83b75..bc9a30af1fac 100644 --- a/src/client/terminals/codeExecution/repl.ts +++ b/src/client/terminals/codeExecution/repl.ts @@ -5,10 +5,11 @@ import { inject, injectable } from 'inversify'; import { Disposable } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IInterpreterService } from '../../interpreter/contracts'; import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; @injectable() @@ -18,9 +19,21 @@ export class ReplProvider extends TerminalCodeExecutionProvider { @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IDisposableRegistry) disposableRegistry: Disposable[], - @inject(IPlatformService) platformService: IPlatformService + @inject(IPlatformService) platformService: IPlatformService, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) applicationShell: IApplicationShell, ) { - super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + super( + terminalServiceFactory, + configurationService, + workspace, + disposableRegistry, + platformService, + interpreterService, + commandManager, + applicationShell, + ); this.terminalTitle = 'REPL'; } } diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 4c5b8034129d..ea444af4d89e 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -6,63 +6,116 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Uri } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { IApplicationShell, ICommandManager, 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 { IConfigurationService, IDisposable, IDisposableRegistry, Resource } from '../../common/types'; +import { Diagnostics, Repl } from '../../common/utils/localize'; +import { showWarningMessage } from '../../common/vscodeApis/windowApis'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceInfo } from '../../logging'; import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; import { ICodeExecutionService } from '../../terminals/types'; +import { EventName } from '../../telemetry/constants'; +import { sendTelemetryEvent } from '../../telemetry'; @injectable() export class TerminalCodeExecutionProvider implements ICodeExecutionService { + private hasRanOutsideCurrentDrive = false; 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 + @inject(IPlatformService) protected readonly platformService: IPlatformService, + @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, + @inject(ICommandManager) protected readonly commandManager: ICommandManager, + @inject(IApplicationShell) protected readonly applicationShell: IApplicationShell, ) {} - public async executeFile(file: Uri) { - await this.setCwdForFileExecution(file); - const { command, args } = await this.getExecuteFileArgs(file, [file.fsPath.fileToCommandArgument()]); + public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { + await this.setCwdForFileExecution(file, options); + const { command, args } = await this.getExecuteFileArgs(file, [ + file.fsPath.fileToCommandArgumentForPythonExt(), + ]); - await this.getTerminalService(file).sendCommand(command, args); + await this.getTerminalService(file, options).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); + await this.initializeRepl(resource); + if (code == 'deprecated') { + // If user is trying to smart send deprecated code show warning + const selection = await showWarningMessage(Diagnostics.invalidSmartSendMessage, Repl.disableSmartSend); + traceInfo(`Selected file contains invalid Python or Deprecated Python 2 code`); + if (selection === Repl.disableSmartSend) { + this.configurationService.updateSetting('REPL.enableREPLSmartSend', false, resource); + } + } else { + await this.getTerminalService(resource).executeCommand(code, true); + } } - public async initializeRepl(resource?: Uri) { - if (this.replActive && (await this.replActive!)) { - await this._terminalService!.show(); + + public async initializeRepl(resource: Resource) { + const terminalService = this.getTerminalService(resource); + if (this.replActive && (await this.replActive)) { + await terminalService.show(); return; } + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Terminal' }); this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); - await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); + let listener: IDisposable; + Promise.race([ + new Promise((resolve) => setTimeout(() => resolve(true), 3000)), + new Promise((resolve) => { + let count = 0; + const terminalDataTimeout = setTimeout(() => { + resolve(true); // Fall back for test case scenarios. + }, 3000); + // Watch TerminalData to see if REPL launched. + listener = this.applicationShell.onDidWriteTerminalData((e) => { + for (let i = 0; i < e.data.length; i++) { + if (e.data[i] === '>') { + count++; + if (count === 3) { + clearTimeout(terminalDataTimeout); + resolve(true); + } + } + } + }); + }), + ]).then(() => { + if (listener) { + listener.dispose(); + } + resolve(true); + }); - // Give python repl time to start before we start sending text. - setTimeout(() => resolve(true), 1000); + await terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); }); + this.disposables.push( + terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + }), + ); 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 interpreter = await this.interpreterService.getActiveInterpreter(resource); + const interpreterPath = interpreter?.path ?? pythonSettings.pythonPath; + const command = this.platformService.isWindows ? interpreterPath.replace(/\\/g, '/') : interpreterPath; const launchArgs = pythonSettings.terminal.launchArgs; return buildPythonExecInfo(command, [...launchArgs, ...args]); } @@ -71,26 +124,34 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { 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 getTerminalService(resource: Resource, options?: { newTerminalPerFile: boolean }): ITerminalService { + return this.terminalServiceFactory.getTerminalService({ + resource, + title: this.terminalTitle, + newTerminalPerFile: options?.newTerminalPerFile, + }); } - private async setCwdForFileExecution(file: Uri) { + private async setCwdForFileExecution(file: Uri, options?: { newTerminalPerFile: boolean }) { 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()}`); + if (fileDirPath.length > 0) { + if (this.platformService.isWindows && /[a-z]\:/i.test(fileDirPath)) { + const currentDrive = + typeof this.workspace.rootPath === 'string' + ? this.workspace.rootPath.replace(/\:.*/g, '') + : undefined; + const fileDrive = fileDirPath.replace(/\:.*/g, ''); + if (fileDrive !== currentDrive || this.hasRanOutsideCurrentDrive) { + this.hasRanOutsideCurrentDrive = true; + await this.getTerminalService(file).sendText(`${fileDrive}:`); + } + } + await this.getTerminalService(file, options).sendText( + `cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`, + ); } } } diff --git a/src/client/terminals/codeExecution/terminalReplWatcher.ts b/src/client/terminals/codeExecution/terminalReplWatcher.ts new file mode 100644 index 000000000000..951961ab6901 --- /dev/null +++ b/src/client/terminals/codeExecution/terminalReplWatcher.ts @@ -0,0 +1,27 @@ +import { Disposable, TerminalShellExecutionStartEvent } from 'vscode'; +import { onDidStartTerminalShellExecution } from '../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +function checkREPLCommand(command: string): undefined | 'manualTerminal' | `runningScript` { + const lower = command.toLowerCase().trimStart(); + if (lower.startsWith('python') || lower.startsWith('py ')) { + const parts = lower.split(' '); + if (parts.length === 1) { + return 'manualTerminal'; + } + return 'runningScript'; + } + return undefined; +} + +export function registerTriggerForTerminalREPL(disposables: Disposable[]): void { + disposables.push( + onDidStartTerminalShellExecution(async (e: TerminalShellExecutionStartEvent) => { + const replType = checkREPLCommand(e.execution.commandLine.value); + if (e.execution.commandLine.isTrusted && replType) { + sendTelemetryEvent(EventName.REPL, undefined, { replType }); + } + }), + ); +} diff --git a/src/client/terminals/envCollectionActivation/deactivateService.ts b/src/client/terminals/envCollectionActivation/deactivateService.ts new file mode 100644 index 000000000000..0758f3e22311 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/deactivateService.ts @@ -0,0 +1,102 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ITerminalManager } from '../../common/application/types'; +import { pathExists } from '../../common/platform/fs-paths'; +import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types'; +import { Resource } from '../../common/types'; +import { waitForCondition } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceVerbose } from '../../logging'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalDeactivateService } from '../types'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +@injectable() +export class TerminalDeactivateService implements ITerminalDeactivateService { + private readonly envVarScript = path.join(_SCRIPTS_DIR, 'printEnvVariablesToFile.py'); + + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(ITerminalHelper) private readonly terminalHelper: ITerminalHelper, + ) {} + + @cache(-1, true) + public async initializeScriptParams(shell: string): Promise { + const location = this.getLocation(shell); + if (!location) { + return; + } + const shellType = identifyShellFromShellPath(shell); + const terminal = this.terminalManager.createTerminal({ + name: `Python ${shellType} Deactivate`, + shellPath: shell, + hideFromUser: true, + cwd: location, + }); + const globalInterpreters = this.interpreterService.getInterpreters().filter((i) => !i.type); + const outputFile = path.join(location, `envVars.txt`); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + const checkIfFileHasBeenCreated = () => pathExists(outputFile); + const stopWatch = new StopWatch(); + const command = this.terminalHelper.buildCommandForTerminal(shellType, interpreterPath, [ + this.envVarScript, + outputFile, + ]); + terminal.sendText(command); + await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); + traceVerbose(`Time taken to get env vars using terminal is ${stopWatch.elapsedTime}ms`); + } + + public async getScriptLocation(shell: string, resource: Resource): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return undefined; + } + return this.getLocation(shell); + } + + private getLocation(shell: string) { + const shellType = identifyShellFromShellPath(shell); + if (!ShellIntegrationShells.includes(shellType)) { + return undefined; + } + return path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType)); + } + + private getShellFolderName(shellType: TerminalShellType): string { + switch (shellType) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.fish: + return 'fish'; + case TerminalShellType.zsh: + return 'zsh'; + case TerminalShellType.bash: + return 'bash'; + default: + throw new Error(`Unsupported shell type ${shellType}`); + } + } +} diff --git a/src/client/terminals/envCollectionActivation/indicatorPrompt.ts b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts new file mode 100644 index 000000000000..5701bf78603e --- /dev/null +++ b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import * as path from 'path'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IPersistentStateFactory, + Resource, +} from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../types'; +import { sleep } from '../../common/utils/async'; +import { isTestExecution } from '../../common/constants'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { useEnvExtension } from '../../envExt/api.internal'; + +export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; + +@injectable() +export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(ITerminalEnvVarCollectionService) + private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService) || useEnvExtension()) { + return; + } + if (!isTestExecution()) { + // Avoid showing prompt until startup completes. + await sleep(6000); + } + this.disposableRegistry.push( + this.terminalManager.onDidOpenTerminal(async (terminal) => { + const hideFromUser = + 'hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser; + const strictEnv = 'strictEnv' in terminal.creationOptions && terminal.creationOptions.strictEnv; + if (hideFromUser || strictEnv || terminal.creationOptions.name) { + // Only show this notification for basic terminals created using the '+' button. + return; + } + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : this.activeResourceService.getActiveResource(); + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const settings = this.configurationService.getSettings(resource); + if (!settings.terminal.activateEnvironment) { + return; + } + if (this.terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)) { + // No need to show notification if terminal prompt already indicates when env is activated. + return; + } + await this.notifyUsers(resource); + }), + ); + } + + private async notifyUsers(resource: Resource): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalEnvCollectionPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.doNotShowAgain]; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter || !interpreter.type) { + return; + } + const terminalPromptName = getPromptName(interpreter); + const environmentType = interpreter.type === PythonEnvType.Conda ? 'Selected conda' : 'Python virtual'; + const selection = await this.appShell.showInformationMessage( + Interpreters.terminalEnvVarCollectionPrompt.format(environmentType, terminalPromptName), + ...prompts, + ); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await notificationPromptEnabled.updateValue(false); + } + } +} + +function getPromptName(interpreter: PythonEnvironment) { + if (interpreter.envName) { + return `"(${interpreter.envName})"`; + } + if (interpreter.envPath) { + return `"(${path.basename(interpreter.envPath)})"`; + } + return 'environment indicator'; +} diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts new file mode 100644 index 000000000000..2ce8d5d5d86a --- /dev/null +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -0,0 +1,515 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { + MarkdownString, + WorkspaceFolder, + GlobalEnvironmentVariableCollection, + EnvironmentVariableScope, + EnvironmentVariableMutatorOptions, + ProgressLocation, +} from 'vscode'; +import { pathExists, normCase } from '../../common/platform/fs-paths'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IPlatformService } from '../../common/platform/types'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { + IExtensionContext, + IExperimentService, + Resource, + IDisposableRegistry, + IConfigurationService, + IPathUtils, +} from '../../common/types'; +import { Interpreters } from '../../common/utils/localize'; +import { traceError, traceInfo, traceLog, traceVerbose, traceWarn } from '../../logging'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { defaultShells } from '../../interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { TerminalShellType } from '../../common/terminal/types'; +import { OSType } from '../../common/utils/platform'; + +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { + IShellIntegrationDetectionService, + ITerminalDeactivateService, + ITerminalEnvVarCollectionService, +} from '../types'; +import { ProgressService } from '../../common/application/progressService'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { registerPythonStartup } from '../pythonStartup'; + +@injectable() +export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { + public readonly supportedWorkspaceTypes = { + untrustedWorkspace: false, + virtualWorkspace: false, + }; + + /** + * Prompts for these shells cannot be set reliably using variables + */ + private noPromptVariableShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + ]; + + private registeredOnce = false; + + /** + * Carries default environment variables for the currently selected shell. + */ + private processEnvVars: EnvironmentVariables | undefined; + + private readonly progressService: ProgressService; + + private separator: string; + + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IExtensionContext) private context: IExtensionContext, + @inject(IApplicationShell) private shell: IApplicationShell, + @inject(IExperimentService) private experimentService: IExperimentService, + @inject(IApplicationEnvironment) private applicationEnvironment: IApplicationEnvironment, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ITerminalDeactivateService) private readonly terminalDeactivateService: ITerminalDeactivateService, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IShellIntegrationDetectionService) + private readonly shellIntegrationDetectionService: IShellIntegrationDetectionService, + @inject(IEnvironmentVariablesProvider) + private readonly environmentVariablesProvider: IEnvironmentVariablesProvider, + ) { + this.separator = platform.osType === OSType.Windows ? ';' : ':'; + this.progressService = new ProgressService(this.shell); + } + + public async activate(resource: Resource): Promise { + try { + if (useEnvExtension()) { + traceVerbose('Ignoring environment variable experiment since env extension is being used'); + this.context.environmentVariableCollection.clear(); + // Needed for shell integration + await registerPythonStartup(this.context); + return; + } + + if (!inTerminalEnvVarExperiment(this.experimentService)) { + this.context.environmentVariableCollection.clear(); + await this.handleMicroVenv(resource); + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this.handleMicroVenv(r); + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + await registerPythonStartup(this.context); + return; + } + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this._applyCollection(r).ignoreErrors(); + }, + this, + this.disposables, + ); + this.shellIntegrationDetectionService.onDidChangeStatus( + async () => { + traceInfo("Shell integration status changed, can confirm it's working."); + await this._applyCollection(undefined).ignoreErrors(); + }, + this, + this.disposables, + ); + this.environmentVariablesProvider.onDidEnvironmentVariablesChange( + async (r: Resource) => { + await this._applyCollection(r).ignoreErrors(); + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + this.processEnvVars = undefined; + // Pass in the shell where known instead of relying on the application environment, because of bug + // on VSCode: https://github.com/microsoft/vscode/issues/160694 + await this._applyCollection(undefined, shell).ignoreErrors(); + }, + this, + this.disposables, + ); + const { shell } = this.applicationEnvironment; + const isActive = await this.shellIntegrationDetectionService.isWorking(); + const shellType = identifyShellFromShellPath(shell); + if (!isActive && shellType !== TerminalShellType.commandPrompt) { + traceWarn( + `Shell integration may not be active, environment activated may be overridden by the shell.`, + ); + } + this.registeredOnce = true; + } + this._applyCollection(resource).ignoreErrors(); + } catch (ex) { + traceError(`Activating terminal env collection failed`, ex); + } + } + + public async _applyCollection(resource: Resource, shell?: string): Promise { + this.progressService.showProgress({ + location: ProgressLocation.Window, + title: Interpreters.activatingTerminals, + }); + await this._applyCollectionImpl(resource, shell).catch((ex) => { + traceError(`Failed to apply terminal env vars`, shell, ex); + return Promise.reject(ex); // Ensures progress indicator does not disappear in case of errors, so we can catch issues faster. + }); + this.progressService.hideProgress(); + } + + private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + const workspaceFolder = this.getWorkspaceFolder(resource); + const settings = this.configurationService.getSettings(resource); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + if (useEnvExtension()) { + envVarCollection.clear(); + traceVerbose('Do not activate terminal env vars as env extension is being used'); + return; + } + + if (!settings.terminal.activateEnvironment) { + envVarCollection.clear(); + traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); + return; + } + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( + resource, + undefined, + undefined, + shell, + ); + const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; + traceVerbose(`Activated environment variables for ${resource?.fsPath}`, env); + if (!env) { + const shellType = identifyShellFromShellPath(shell); + const defaultShell = defaultShells[this.platform.osType]; + if (defaultShell?.shellType !== shellType) { + // Commands to fetch env vars may fail in custom shells due to unknown reasons, in that case + // fallback to default shells as they are known to work better. + await this._applyCollectionImpl(resource, defaultShell?.shell); + return; + } + await this.trackTerminalPrompt(shell, resource, env); + envVarCollection.clear(); + this.processEnvVars = undefined; + return; + } + if (!this.processEnvVars) { + this.processEnvVars = await this.environmentActivationService.getProcessEnvironmentVariables( + resource, + shell, + ); + } + const processEnv = normCaseKeys(this.processEnvVars); + + // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. + env.PS1 = await this.getPS1(shell, resource, env); + const defaultPrependOptions = await this.getPrependOptions(); + + // Clear any previously set env vars from collection + envVarCollection.clear(); + const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); + Object.keys(env).forEach((key) => { + if (shouldSkip(key)) { + return; + } + let value = env[key]; + const prevValue = processEnv[key]; + if (prevValue !== value) { + if (value !== undefined) { + if (key === 'PS1') { + // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. + traceLog( + `Prepending environment variable ${key} in collection with ${value} ${JSON.stringify( + defaultPrependOptions, + )}`, + ); + envVarCollection.prepend(key, value, defaultPrependOptions); + return; + } + if (key === 'PATH') { + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) { + // Prefer prepending to PATH instead of replacing it, as we do not want to replace any + // changes to PATH users might have made it in their init scripts (~/.bashrc etc.) + value = env.PATH.slice(0, -processEnv.PATH.length); + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } + traceLog( + `Prepending environment variable ${key} in collection with ${value} ${JSON.stringify( + options, + )}`, + ); + envVarCollection.prepend(key, value, options); + } else { + if (!value.endsWith(this.separator)) { + value = value.concat(this.separator); + } + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } + traceLog( + `Prepending environment variable ${key} in collection to ${value} ${JSON.stringify( + options, + )}`, + ); + envVarCollection.prepend(key, value, options); + } + return; + } + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + traceLog( + `Setting environment variable ${key} in collection to ${value} ${JSON.stringify(options)}`, + ); + envVarCollection.replace(key, value, options); + } + } + }); + + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); + const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); + envVarCollection.description = description; + + await this.trackTerminalPrompt(shell, resource, env); + await this.terminalDeactivateService.initializeScriptParams(shell).catch((ex) => { + traceError(`Failed to initialize deactivate script`, shell, ex); + }); + } + + private isPromptSet = new Map(); + + // eslint-disable-next-line class-methods-use-this + public isTerminalPromptSetCorrectly(resource?: Resource): boolean { + const workspaceFolder = this.getWorkspaceFolder(resource); + return !!this.isPromptSet.get(workspaceFolder?.index); + } + + /** + * Call this once we know terminal prompt is set correctly for terminal owned by this resource. + */ + private terminalPromptIsCorrect(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.set(key, true); + } + + private terminalPromptIsUnknown(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.delete(key); + } + + /** + * Tracks whether prompt for terminal was correctly set. + */ + private async trackTerminalPrompt(shell: string, resource: Resource, env: EnvironmentVariables | undefined) { + this.terminalPromptIsUnknown(resource); + if (!env) { + this.terminalPromptIsCorrect(resource); + return; + } + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { + // PS1 should be set but no PS1 was set. + return; + } + const config = await this.shellIntegrationDetectionService.isWorking(); + if (!config) { + traceVerbose('PS1 is not set when shell integration is disabled.'); + return; + } + } + this.terminalPromptIsCorrect(resource); + } + + private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { + // PS1 returned by shell is not predictable: #22078 + // Hence calculate it ourselves where possible. Should no longer be needed once #22128 is available. + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return env.PS1; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1) { + const prompt = getPromptForEnv(interpreter, env); + if (prompt) { + return prompt; + } + } + } + if (env.PS1) { + // Prefer PS1 set by env vars, as env.PS1 may or may not contain the full PS1: #22056. + return env.PS1; + } + return undefined; + } + + private async handleMicroVenv(resource: Resource) { + try { + const settings = this.configurationService.getSettings(resource); + const workspaceFolder = this.getWorkspaceFolder(resource); + if (useEnvExtension()) { + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + traceVerbose('Do not activate microvenv as env extension is being used'); + return; + } + if (!settings.terminal.activateEnvironment) { + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + traceVerbose( + 'Do not activate microvenv as activating environments in terminal is disabled for', + resource?.fsPath, + ); + return; + } + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.envType === EnvironmentType.Venv) { + const activatePath = path.join(path.dirname(interpreter.path), 'activate'); + if (!(await pathExists(activatePath))) { + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + const pathVarName = getSearchPathEnvVarNames()[0]; + envVarCollection.replace( + 'PATH', + `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, + ); + return; + } + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + } + } catch (ex) { + traceWarn(`Microvenv failed as it is using proposed API which is constantly changing`, ex); + } + } + + private async getPrependOptions(): Promise { + const isActive = await this.shellIntegrationDetectionService.isWorking(); + // Ideally we would want to prepend exactly once, either at shell integration or process creation. + // TODO: Stop prepending altogether once https://github.com/microsoft/vscode/issues/145234 is available. + return isActive + ? { + applyAtShellIntegration: true, + applyAtProcessCreation: false, + } + : { + applyAtShellIntegration: true, // Takes care of false negatives in case manual integration is being used. + applyAtProcessCreation: true, + }; + } + + private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { + const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; + return envVarCollection.getScoped(scope); + } + + private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { + let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if ( + !workspaceFolder && + Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length > 0 + ) { + [workspaceFolder] = this.workspaceService.workspaceFolders; + } + return workspaceFolder; + } +} + +function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (env.PS1) { + // Activated variables contain PS1, meaning it was supposed to be set. + return true; + } + if (type === PythonEnvType.Virtual) { + const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; + const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; + return !isPromptDisabled; + } + if (type === PythonEnvType.Conda) { + // Instead of checking config value using `conda config --get changeps1`, simply check + // `CONDA_PROMPT_MODIFER` to avoid the cost of launching the conda binary. + const promptEnabledVar = env.CONDA_PROMPT_MODIFIER; + const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; + return !!isPromptEnabled; + } + return false; +} + +function shouldSkip(env: string) { + return [ + '_', + 'SHLVL', + // Even though this maybe returned, setting it can result in output encoding errors in terminal. + 'PYTHONUTF8', + // We have deactivate service which takes care of setting it. + '_OLD_VIRTUAL_PATH', + 'PWD', + ].includes(env); +} + +function getPromptForEnv(interpreter: PythonEnvironment | undefined, env: EnvironmentVariables) { + if (!interpreter) { + return undefined; + } + if (interpreter.envName) { + if (interpreter.envName === 'base') { + // If conda base environment is selected, it can lead to "(base)" appearing twice if we return the env name. + return undefined; + } + if (interpreter.type === PythonEnvType.Virtual && env.VIRTUAL_ENV_PROMPT) { + return `${env.VIRTUAL_ENV_PROMPT}`; + } + return `(${interpreter.envName}) `; + } + if (interpreter.envPath) { + return `(${path.basename(interpreter.envPath)}) `; + } + return undefined; +} + +function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const result: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + result[normCase(key)] = env[key]; + }); + return result; +} diff --git a/src/client/terminals/envCollectionActivation/shellIntegrationService.ts b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts new file mode 100644 index 000000000000..92bb98029892 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable, inject } from 'inversify'; +import { EventEmitter } from 'vscode'; +import { + IApplicationEnvironment, + IApplicationShell, + ITerminalManager, + IWorkspaceService, +} from '../../common/application/types'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import { traceError, traceVerbose } from '../../logging'; +import { IShellIntegrationDetectionService } from '../types'; +import { isTrusted } from '../../common/vscodeApis/workspaceApis'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +export enum isShellIntegrationWorking { + key = 'SHELL_INTEGRATION_WORKING_KEY', +} + +@injectable() +export class ShellIntegrationDetectionService implements IShellIntegrationDetectionService { + private isWorkingForShell = new Set(); + + private readonly didChange = new EventEmitter(); + + private isDataWriteEventWorking = true; + + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) { + try { + const activeShellType = identifyShellFromShellPath(this.appEnvironment.shell); + const key = getKeyForShell(activeShellType); + const persistedResult = this.persistentStateFactory.createGlobalPersistentState(key); + if (persistedResult.value) { + this.isWorkingForShell.add(activeShellType); + } + this.appShell.onDidWriteTerminalData( + (e) => { + if (e.data.includes('\x1b]633;A\x07') || e.data.includes('\x1b]133;A\x07')) { + let { shell } = this.appEnvironment; + if ('shellPath' in e.terminal.creationOptions && e.terminal.creationOptions.shellPath) { + shell = e.terminal.creationOptions.shellPath; + } + const shellType = identifyShellFromShellPath(shell); + traceVerbose('Received shell integration sequence for', shellType); + const wasWorking = this.isWorkingForShell.has(shellType); + this.isWorkingForShell.add(shellType); + if (!wasWorking) { + // If it wasn't working previously, status has changed. + this.didChange.fire(); + } + } + }, + this, + this.disposables, + ); + this.appEnvironment.onDidChangeShell( + async (shell: string) => { + this.createDummyHiddenTerminal(shell); + }, + this, + this.disposables, + ); + this.createDummyHiddenTerminal(this.appEnvironment.shell); + } catch (ex) { + this.isDataWriteEventWorking = false; + traceError('Unable to check if shell integration is active', ex); + } + const isEnabled = !!this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled'); + if (!isEnabled) { + traceVerbose('Shell integration is disabled in user settings.'); + } + } + + public readonly onDidChangeStatus = this.didChange.event; + + public async isWorking(): Promise { + const { shell } = this.appEnvironment; + return this._isWorking(shell).catch((ex) => { + traceError(`Failed to determine if shell supports shell integration`, shell, ex); + return false; + }); + } + + public async _isWorking(shell: string): Promise { + const shellType = identifyShellFromShellPath(shell); + const isSupposedToWork = ShellIntegrationShells.includes(shellType); + if (!isSupposedToWork) { + return false; + } + const key = getKeyForShell(shellType); + const persistedResult = this.persistentStateFactory.createGlobalPersistentState(key); + if (persistedResult.value !== undefined) { + return persistedResult.value; + } + const result = await this.useDataWriteApproach(shellType); + if (result) { + // Once we know that shell integration is working for a shell, persist it so we need not do this check every session. + await persistedResult.updateValue(result); + } + return result; + } + + private async useDataWriteApproach(shellType: TerminalShellType) { + // For now, based on problems with using the command approach, use terminal data write event. + if (!this.isDataWriteEventWorking) { + // Assume shell integration is working, if data write event isn't working. + return true; + } + if (shellType === TerminalShellType.powershell || shellType === TerminalShellType.powershellCore) { + // Due to upstream bug: https://github.com/microsoft/vscode/issues/204616, assume shell integration is working for now. + return true; + } + if (!this.isWorkingForShell.has(shellType)) { + // Maybe data write event has not been processed yet, wait a bit. + await sleep(1000); + } + traceVerbose( + 'Did we determine shell integration to be working for', + shellType, + '?', + this.isWorkingForShell.has(shellType), + ); + return this.isWorkingForShell.has(shellType); + } + + /** + * Creates a dummy terminal so that we are guaranteed a data write event for this shell type. + */ + private createDummyHiddenTerminal(shell: string) { + if (isTrusted()) { + this.terminalManager.createTerminal({ + shellPath: shell, + hideFromUser: true, + }); + } + } +} + +function getKeyForShell(shellType: TerminalShellType) { + return `${isShellIntegrationWorking.key}_${shellType}`; +} diff --git a/src/client/terminals/pythonStartup.ts b/src/client/terminals/pythonStartup.ts new file mode 100644 index 000000000000..b6f68c860b46 --- /dev/null +++ b/src/client/terminals/pythonStartup.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ExtensionContext, MarkdownString, Uri } from 'vscode'; +import * as path from 'path'; +import { copy, createDirectory, getConfiguration, onDidChangeConfiguration } from '../common/vscodeApis/workspaceApis'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { Interpreters } from '../common/utils/localize'; + +async function applyPythonStartupSetting(context: ExtensionContext): Promise { + const config = getConfiguration('python'); + const pythonrcSetting = config.get('terminal.shellIntegration.enabled'); + + if (pythonrcSetting) { + const storageUri = context.storageUri || context.globalStorageUri; + try { + await createDirectory(storageUri); + } catch { + // already exists, most likely + } + const destPath = Uri.joinPath(storageUri, 'pythonrc.py'); + const sourcePath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'pythonrc.py'); + await copy(Uri.file(sourcePath), destPath, { overwrite: true }); + context.environmentVariableCollection.replace('PYTHONSTARTUP', destPath.fsPath); + // When shell integration is enabled, we disable PyREPL from cpython. + context.environmentVariableCollection.replace('PYTHON_BASIC_REPL', '1'); + context.environmentVariableCollection.description = new MarkdownString( + Interpreters.shellIntegrationEnvVarCollectionDescription, + ); + } else { + context.environmentVariableCollection.delete('PYTHONSTARTUP'); + context.environmentVariableCollection.delete('PYTHON_BASIC_REPL'); + context.environmentVariableCollection.description = new MarkdownString( + Interpreters.shellIntegrationDisabledEnvVarCollectionDescription, + ); + } +} + +export async function registerPythonStartup(context: ExtensionContext): Promise { + await applyPythonStartupSetting(context); + context.subscriptions.push( + onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration('python.terminal.shellIntegration.enabled')) { + await applyPythonStartupSetting(context); + } + }), + ); +} diff --git a/src/client/terminals/pythonStartupLinkProvider.ts b/src/client/terminals/pythonStartupLinkProvider.ts new file mode 100644 index 000000000000..aba1270f1412 --- /dev/null +++ b/src/client/terminals/pythonStartupLinkProvider.ts @@ -0,0 +1,50 @@ +/* eslint-disable class-methods-use-this */ +import { + CancellationToken, + Disposable, + ProviderResult, + TerminalLink, + TerminalLinkContext, + TerminalLinkProvider, +} from 'vscode'; +import { executeCommand } from '../common/vscodeApis/commandApis'; +import { registerTerminalLinkProvider } from '../common/vscodeApis/windowApis'; +import { Repl } from '../common/utils/localize'; + +interface CustomTerminalLink extends TerminalLink { + command: string; +} + +export class CustomTerminalLinkProvider implements TerminalLinkProvider { + provideTerminalLinks( + context: TerminalLinkContext, + _token: CancellationToken, + ): ProviderResult { + const links: CustomTerminalLink[] = []; + let expectedNativeLink; + + if (process.platform === 'darwin') { + expectedNativeLink = 'Cmd click to launch VS Code Native REPL'; + } else { + expectedNativeLink = 'Ctrl click to launch VS Code Native REPL'; + } + + if (context.line.includes(expectedNativeLink)) { + links.push({ + startIndex: context.line.indexOf(expectedNativeLink), + length: expectedNativeLink.length, + tooltip: Repl.launchNativeRepl, + command: 'python.startNativeREPL', + }); + } + return links; + } + + async handleTerminalLink(link: CustomTerminalLink): Promise { + await executeCommand(link.command); + } +} + +export function registerCustomTerminalLinkProvider(disposables: Disposable[]): void { + disposables.push(registerTerminalLinkProvider(new CustomTerminalLinkProvider())); +} diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index 619c69eab7c0..e62701dcec0e 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -1,26 +1,29 @@ // 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 { IServiceManager } from '../ioc/types'; +import { 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'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + IShellIntegrationDetectionService, + ITerminalAutoActivation, + ITerminalDeactivateService, + ITerminalEnvVarCollectionService, +} from './types'; +import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; +import { TerminalDeactivateService } from './envCollectionActivation/deactivateService'; +import { ShellIntegrationDetectionService } from './envCollectionActivation/shellIntegrationService'; -interface IServiceRegistry { - addSingleton( - serviceIdentifier: interfaces.ServiceIdentifier, - constructor: ClassType, - name?: string | number | symbol - ): void; -} - -export function registerTypes(serviceManager: IServiceRegistry) { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -28,19 +31,29 @@ export function registerTypes(serviceManager: IServiceRegistry) { serviceManager.addSingleton( ICodeExecutionService, DjangoShellCodeExecutionProvider, - 'djangoShell' + 'djangoShell', ); serviceManager.addSingleton( ICodeExecutionService, TerminalCodeExecutionProvider, - 'standard' + 'standard', ); serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); - + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton(ITerminalDeactivateService, TerminalDeactivateService); serviceManager.addSingleton( IExtensionSingleActivationService, - ExtensionActivationForTerminalActivation + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton( + IShellIntegrationDetectionService, + ShellIntegrationDetectionService, ); + + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index b096153a3db8..1384057c3b7c 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -1,34 +1,60 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, TextEditor, Uri } from 'vscode'; -import { IDisposable } from '../common/types'; +import { Event, Terminal, TextEditor, Uri } from 'vscode'; +import { IDisposable, Resource } from '../common/types'; +import { ReplType } from '../repl/types'; export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri): Promise; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; initializeRepl(resource?: Uri): Promise; } export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): Promise; + normalizeLines(code: string, replType: ReplType, wholeFileContent?: string, resource?: Uri): Promise; getFileToExecute(): Promise; - saveFileIfDirty(file: Uri): 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; + disableAutoActivation(terminal: Terminal): void; +} + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} + +export const IShellIntegrationDetectionService = Symbol('IShellIntegrationDetectionService'); +export interface IShellIntegrationDetectionService { + onDidChangeStatus: Event; + isWorking(): Promise; +} + +export const ITerminalDeactivateService = Symbol('ITerminalDeactivateService'); +export interface ITerminalDeactivateService { + initializeScriptParams(shell: string): Promise; + getScriptLocation(shell: string, resource: Resource): Promise; +} + +export const IPythonStartupEnvVarService = Symbol('IPythonStartupEnvVarService'); +export interface IPythonStartupEnvVarService { + register(): void; } diff --git a/src/client/testing/codeLenses/main.ts b/src/client/testing/codeLenses/main.ts deleted file mode 100644 index 2179c10d2ec8..000000000000 --- a/src/client/testing/codeLenses/main.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 269f7f7d4539..000000000000 --- a/src/client/testing/codeLenses/testFiles.ts +++ /dev/null @@ -1,315 +0,0 @@ -'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 deleted file mode 100644 index 2633f99ab50c..000000000000 --- a/src/client/testing/common/argumentsHelper.ts +++ /dev/null @@ -1,124 +0,0 @@ -// 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/bufferedTestConfigSettingService.ts b/src/client/testing/common/bufferedTestConfigSettingService.ts new file mode 100644 index 000000000000..35266de38c72 --- /dev/null +++ b/src/client/testing/common/bufferedTestConfigSettingService.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; + +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[]): Promise { + this.ops.push(['updateTestArgs', testDirectory, product, args]); + return Promise.resolve(); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + this.ops.push(['enable', testDirectory, product, []]); + return Promise.resolve(); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + this.ops.push(['disable', testDirectory, product, []]); + return Promise.resolve(); + } + + public async apply(cfg: ITestConfigSettingsService): Promise { + const { ops } = this; + 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; + } + } + return Promise.resolve(); + } + + // eslint-disable-next-line class-methods-use-this + public getTestEnablingSetting(_: UnitTestProduct): string { + throw new Error('Method not implemented.'); + } +} diff --git a/src/client/testing/common/configSettingService.ts b/src/client/testing/common/configSettingService.ts new file mode 100644 index 000000000000..f6cfeee773e5 --- /dev/null +++ b/src/client/testing/common/configSettingService.ts @@ -0,0 +1,77 @@ +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, 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[]): Promise { + 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); + } + + // eslint-disable-next-line class-methods-use-this + public getTestEnablingSetting(product: UnitTestProduct): string { + switch (product) { + case Product.unittest: + return 'testing.unittestEnabled'; + case Product.pytest: + return 'testing.pytestEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + + // eslint-disable-next-line class-methods-use-this + private getTestArgSetting(product: UnitTestProduct): string { + switch (product) { + case Product.unittest: + return 'testing.unittestArgs'; + case Product.pytest: + return 'testing.pytestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + + private async updateSetting(testDirectory: string | Uri, setting: string, value: unknown) { + let pythonConfig: WorkspaceConfiguration; + const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!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})`); + } + + pythonConfig = this.workspaceService.getConfiguration('python', workspaceFolder.uri); + } + + return pythonConfig.update(setting, value); + } +} diff --git a/src/client/testing/common/constants.ts b/src/client/testing/common/constants.ts index 002b250d58c5..4f41e60c8806 100644 --- a/src/client/testing/common/constants.ts +++ b/src/client/testing/common/constants.ts @@ -1,24 +1,7 @@ import { Product } from '../../common/types'; -import { TestProvider, UnitTestProduct } from './types'; +import { TestProvider } from '../types'; +import { 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 UNIT_TEST_PRODUCTS: UnitTestProduct[] = [Product.pytest, Product.unittest]; 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 index 969c02e19eb7..037bfb265088 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -1,81 +1,158 @@ 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 { DebugConfiguration, l10n, Uri, WorkspaceFolder, DebugSession, DebugSessionOptions, Disposable } from 'vscode'; +import { IApplicationShell, IDebugService } 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 { DebuggerTypeName, PythonDebuggerTypeName } from '../../debugger/constants'; import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; -import { LaunchRequestArguments } from '../../debugger/types'; +import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; import { IServiceContainer } from '../../ioc/types'; -import { ITestDebugConfig, ITestDebugLauncher, LaunchOptions, TestProvider } from './types'; +import { traceError, traceVerbose } from '../../logging'; +import { TestProvider } from '../types'; +import { ITestDebugLauncher, LaunchOptions } from './types'; +import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; +import { showErrorMessage } from '../../common/vscodeApis/windowApis'; +import { createDeferred } from '../../common/utils/async'; +import { addPathToPythonpath } from './helpers'; +import * as envExtApi from '../../envExt/api.internal'; + +/** + * Key used to mark debug configurations with a unique session identifier. + * This allows us to track which debug session belongs to which launchDebugger() call + * when multiple debug sessions are launched in parallel. + */ +const TEST_SESSION_MARKER_KEY = '__vscodeTestSessionMarker'; @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 + 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; + /** + * Launches a debug session for test execution. + * Handles cancellation, multi-session support via unique markers, and cleanup. + */ + public async launchDebugger( + options: LaunchOptions, + callback?: () => void, + sessionOptions?: DebugSessionOptions, + ): Promise { + const deferred = createDeferred(); + let hasCallbackBeenCalled = false; + + // Collect disposables for cleanup when debugging completes + const disposables: Disposable[] = []; + + // Ensure callback is only invoked once, even if multiple termination paths fire + const callCallbackOnce = () => { + if (!hasCallbackBeenCalled) { + hasCallbackBeenCalled = true; + callback?.(); + } + }; + + // Early exit if already cancelled before we start + if (options.token && options.token.isCancellationRequested) { + callCallbackOnce(); + deferred.resolve(); + return deferred.promise; + } + + // Listen for cancellation from the test run (e.g., user clicks stop in Test Explorer) + // This allows the caller to clean up resources even if the debug session is still running + if (options.token) { + disposables.push( + options.token.onCancellationRequested(() => { + deferred.resolve(); + callCallbackOnce(); + }), + ); } - const workspaceFolder = this.resolveWorkspaceFolder(options.cwd); + const workspaceFolder = DebugLauncher.resolveWorkspaceFolder(options.cwd); const launchArgs = await this.getLaunchArgs( options, workspaceFolder, - this.configService.getSettings(workspaceFolder.uri) + 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 []; - } + + // Unique marker to identify this session among concurrent debug sessions + const sessionMarker = `test-${Date.now()}-${Math.random().toString(36).slice(2)}`; + launchArgs[TEST_SESSION_MARKER_KEY] = sessionMarker; + + let ourSession: DebugSession | undefined; + + // Capture our specific debug session when it starts by matching the marker. + // This fires for ALL debug sessions, so we filter to only our marker. + disposables.push( + debugManager.onDidStartDebugSession((session) => { + if (session.configuration[TEST_SESSION_MARKER_KEY] === sessionMarker) { + ourSession = session; + traceVerbose(`[test-debug] Debug session started: ${session.name} (${session.id})`); + } + }), + ); + + // Handle debug session termination (user stops debugging, or tests complete). + // Only react to OUR session terminating - other parallel sessions should + // continue running independently. + disposables.push( + debugManager.onDidTerminateDebugSession((session) => { + if (ourSession && session.id === ourSession.id) { + traceVerbose(`[test-debug] Debug session terminated: ${session.name} (${session.id})`); + deferred.resolve(); + callCallbackOnce(); + } + }), + ); + + // Clean up event subscriptions when debugging completes (success, failure, or cancellation) + deferred.promise.finally(() => { + disposables.forEach((d) => d.dispose()); + }); + + // Start the debug session + let started = false; 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 []; + started = await debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions); + } catch (error) { + traceError('Error starting debug session', error); + deferred.reject(error); + callCallbackOnce(); + return deferred.promise; } + if (!started) { + traceError('Failed to start debug session'); + deferred.resolve(); + callCallbackOnce(); + } + + return deferred.promise; } - private resolveWorkspaceFolder(cwd: string): WorkspaceFolder { - if (!this.workspaceService.hasWorkspaceFolders) { + + private static resolveWorkspaceFolder(cwd: string): WorkspaceFolder { + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; + if (!hasWorkspaceFolders) { throw new Error('Please open a workspace'); } const cwdUri = cwd ? Uri.file(cwd) : undefined; - let workspaceFolder = this.workspaceService.getWorkspaceFolder(cwdUri); + let workspaceFolder = getWorkspaceFolder(cwdUri); if (!workspaceFolder) { - workspaceFolder = this.workspaceService.workspaceFolders![0]; + const [first] = getWorkspaceFolders()!; + workspaceFolder = first; } return workspaceFolder; } @@ -83,50 +160,89 @@ export class DebugLauncher implements ITestDebugLauncher { private async getLaunchArgs( options: LaunchOptions, workspaceFolder: WorkspaceFolder, - configSettings: IPythonSettings + configSettings: IPythonSettings, ): Promise { - let debugConfig = await this.readDebugConfig(workspaceFolder); + let debugConfig = await DebugLauncher.readDebugConfig(workspaceFolder); if (!debugConfig) { debugConfig = { name: 'Debug Unit Test', - type: 'python', + type: 'debugpy', request: 'test', - subProcess: true + subProcess: true, }; } + + // Use project name in debug session name if provided + if (options.project) { + debugConfig.name = `Debug Tests: ${options.project.name}`; + } + if (!debugConfig.rules) { debugConfig.rules = []; } debugConfig.rules.push({ - path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), - include: false + path: path.join(EXTENSION_ROOT_DIR, 'python_files'), + include: false, }); - this.applyDefaults(debugConfig!, workspaceFolder, configSettings); + + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings, options.cwd); 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; + public async readAllDebugConfigs(workspace: WorkspaceFolder): Promise { + try { + const configs = await getConfigurationsForWorkspace(workspace); + return configs; + } catch (exc) { + traceError('could not get debug config', exc); + const appShell = this.serviceContainer.get(IApplicationShell); + await appShell.showErrorMessage( + l10n.t('Could not load unit test config from launch.json as it is missing a field'), + ); + return []; + } + } + + private static async readDebugConfig( + workspaceFolder: WorkspaceFolder, + ): Promise { + try { + const configs = await getConfigurationsForWorkspace(workspaceFolder); + for (const cfg of configs) { + if ( + cfg.name && + (cfg.type === DebuggerTypeName || cfg.type === PythonDebuggerTypeName) && + (cfg.request === 'test' || + (cfg as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugTest)) + ) { + // Return the first one. + return cfg as LaunchRequestArguments; + } } - // Return the first one. - return cfg as ITestDebugConfig; + return undefined; + } catch (exc) { + traceError('could not get debug config', exc); + await showErrorMessage(l10n.t('Could not load unit test config from launch.json as it is missing a field')); + return undefined; } - return undefined; } - private applyDefaults(cfg: ITestDebugConfig, workspaceFolder: WorkspaceFolder, configSettings: IPythonSettings) { + + private static applyDefaults( + cfg: LaunchRequestArguments, + workspaceFolder: WorkspaceFolder, + configSettings: IPythonSettings, + optionsCwd?: string, + ) { // 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; + // For project-based testing, use the project's cwd (optionsCwd) if provided. + // Otherwise fall back to settings.testing.cwd or the workspace folder. + cfg.cwd = optionsCwd || configSettings.testing.cwd || workspaceFolder.uri.fsPath; } if (!cfg.env) { cfg.env = {}; @@ -134,7 +250,6 @@ export class DebugLauncher implements ITestDebugLauncher { if (!cfg.envFile) { cfg.envFile = configSettings.envFile; } - if (cfg.stopOnEntry === undefined) { cfg.stopOnEntry = false; } @@ -151,48 +266,96 @@ export class DebugLauncher implements ITestDebugLauncher { } private async convertConfigToArgs( - debugConfig: ITestDebugConfig, + debugConfig: LaunchRequestArguments, workspaceFolder: WorkspaceFolder, - options: LaunchOptions + options: LaunchOptions, ): Promise { const configArgs = debugConfig as LaunchRequestArguments; - - const testArgs = this.fixArgs(options.args, options.testProvider); - const script = this.getTestLauncherScript(options.testProvider); + const testArgs = + options.testProvider === 'unittest' ? options.args.filter((item) => item !== '--debug') : options.args; + const script = DebugLauncher.getTestLauncherScript(options.testProvider); const args = script(testArgs); - configArgs.program = args[0]; + const [program] = args; + configArgs.program = program; + configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. - const launchArgs = await this.launchResolver.resolveDebugConfiguration( + let launchArgs = await this.launchResolver.resolveDebugConfiguration( workspaceFolder, configArgs, - options.token + options.token, + ); + if (!launchArgs) { + throw Error(`Invalid debug config "${debugConfig.name}"`); + } + launchArgs = await this.launchResolver.resolveDebugConfigurationWithSubstitutedVariables( + workspaceFolder, + launchArgs, + 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'); + if (options.pytestPort && options.runTestIdsPort) { + launchArgs.env = { + ...launchArgs.env, + TEST_RUN_PIPE: options.pytestPort, + RUN_TEST_IDS_PIPE: options.runTestIdsPort, + }; } else { - return args; + throw Error( + `Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`, + ); } + + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + // check if PYTHONPATH is already set in the environment variables + if (launchArgs.env) { + const additionalPythonPath = [pluginPath]; + if (launchArgs.cwd) { + additionalPythonPath.push(launchArgs.cwd); + } else if (options.cwd) { + additionalPythonPath.push(options.cwd); + } + // add the plugin path or cwd to PYTHONPATH if it is not already there using the following function + // this function will handle if PYTHONPATH is undefined + addPathToPythonpath(additionalPythonPath, launchArgs.env.PYTHONPATH); + } + + // Clear out purpose so we can detect if the configuration was used to + // run via F5 style debugging. + launchArgs.purpose = []; + + // For project-based execution, get the Python path from the project's environment. + // Fallback: if env API unavailable or fails, LaunchConfigurationResolver already set + // launchArgs.python from the active interpreter, so debugging still works. + if (options.project && envExtApi.useEnvExtension()) { + try { + const pythonEnv = await envExtApi.getEnvironment(options.project.uri); + if (pythonEnv?.execInfo?.run?.executable) { + launchArgs.python = pythonEnv.execInfo.run.executable; + traceVerbose( + `[test-by-project] Debug session using Python path from project: ${launchArgs.python}`, + ); + } + } catch (error) { + traceVerbose(`[test-by-project] Could not get environment for project, using default: ${error}`); + } + } + + return launchArgs; } - private getTestLauncherScript(testProvider: TestProvider) { + private static getTestLauncherScript(testProvider: TestProvider) { switch (testProvider) { case 'unittest': { - return internalScripts.visualstudio_py_testlauncher; + return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger } - case 'pytest': - case 'nosetest': { - return internalScripts.testlauncher; + case 'pytest': { + return internalScripts.pytestlauncher; // this is the new way to run pytest execution, debugger } default: { throw new Error(`Unknown test provider '${testProvider}'`); diff --git a/src/client/testing/common/enablementTracker.ts b/src/client/testing/common/enablementTracker.ts deleted file mode 100644 index fe6b319873ec..000000000000 --- a/src/client/testing/common/enablementTracker.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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/helpers.ts b/src/client/testing/common/helpers.ts new file mode 100644 index 000000000000..021849277b33 --- /dev/null +++ b/src/client/testing/common/helpers.ts @@ -0,0 +1,37 @@ +import * as path from 'path'; + +/** + * This function normalizes the provided paths and the existing paths in PYTHONPATH, + * adds the provided paths to PYTHONPATH if they're not already present, + * and then returns the updated PYTHONPATH. + * + * @param newPaths - An array of paths to be added to PYTHONPATH + * @param launchPythonPath - The initial PYTHONPATH + * @returns The updated PYTHONPATH + */ +export function addPathToPythonpath(newPaths: string[], launchPythonPath: string | undefined): string { + // Split PYTHONPATH into array of paths if it exists + let paths: string[]; + if (!launchPythonPath) { + paths = []; + } else { + paths = launchPythonPath.split(path.delimiter); + } + + // Normalize each path in the existing PYTHONPATH + paths = paths.map((p) => path.normalize(p)); + + // Normalize each new path and add it to PYTHONPATH if it's not already present + newPaths.forEach((newPath) => { + const normalizedNewPath: string = path.normalize(newPath); + + if (!paths.includes(normalizedNewPath)) { + paths.push(normalizedNewPath); + } + }); + + // Join the paths with ':' to create the updated PYTHONPATH + const updatedPythonPath: string = paths.join(path.delimiter); + + return updatedPythonPath; +} diff --git a/src/client/testing/common/managers/baseTestManager.ts b/src/client/testing/common/managers/baseTestManager.ts deleted file mode 100644 index ded6581f62cf..000000000000 --- a/src/client/testing/common/managers/baseTestManager.ts +++ /dev/null @@ -1,516 +0,0 @@ -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 deleted file mode 100644 index 22c19e0b821d..000000000000 --- a/src/client/testing/common/managers/testConfigurationManager.ts +++ /dev/null @@ -1,128 +0,0 @@ -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 deleted file mode 100644 index fd2f3145b7d8..000000000000 --- a/src/client/testing/common/runner.ts +++ /dev/null @@ -1,140 +0,0 @@ -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 deleted file mode 100644 index 3fd888620750..000000000000 --- a/src/client/testing/common/services/configSettingService.ts +++ /dev/null @@ -1,119 +0,0 @@ -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 deleted file mode 100644 index 40b70561b649..000000000000 --- a/src/client/testing/common/services/contextService.ts +++ /dev/null @@ -1,54 +0,0 @@ -// 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 deleted file mode 100644 index 200c50cd88bd..000000000000 --- a/src/client/testing/common/services/discoveredTestParser.ts +++ /dev/null @@ -1,384 +0,0 @@ -// 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 deleted file mode 100644 index 0042f5e003f0..000000000000 --- a/src/client/testing/common/services/discovery.ts +++ /dev/null @@ -1,63 +0,0 @@ -// 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 deleted file mode 100644 index 46aafe1c74de..000000000000 --- a/src/client/testing/common/services/storageService.ts +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 42a96877ef0c..000000000000 --- a/src/client/testing/common/services/testManagerService.ts +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 0112b3599521..000000000000 --- a/src/client/testing/common/services/testResultsService.ts +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index b2094a3e26b1..000000000000 --- a/src/client/testing/common/services/testsStatusService.ts +++ /dev/null @@ -1,96 +0,0 @@ -// 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 deleted file mode 100644 index e2b929379bbb..000000000000 --- a/src/client/testing/common/services/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index 5e88a6df615b..000000000000 --- a/src/client/testing/common/services/unitTestDiagnosticService.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index e0061997e8dd..000000000000 --- a/src/client/testing/common/services/workspaceTestManagerService.ts +++ /dev/null @@ -1,63 +0,0 @@ -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/testConfigurationManager.ts b/src/client/testing/common/testConfigurationManager.ts new file mode 100644 index 000000000000..be3f0109da02 --- /dev/null +++ b/src/client/testing/common/testConfigurationManager.ts @@ -0,0 +1,125 @@ +import * as path from 'path'; +import { QuickPickItem, QuickPickOptions, Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IInstaller } from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { IServiceContainer } from '../../ioc/types'; +import { traceVerbose } from '../../logging'; +import { UNIT_TEST_PRODUCTS } from './constants'; +import { ITestConfigSettingsService, ITestConfigurationManager, UnitTestProduct } from './types'; + +function handleCancelled(): void { + traceVerbose('testing configuration (in UI) cancelled'); + throw Error('cancelled'); +} + +export abstract class TestConfigurationManager implements ITestConfigurationManager { + protected readonly installer: IInstaller; + + protected readonly testConfigSettingsService: ITestConfigSettingsService; + + private readonly handleCancelled = handleCancelled; + + constructor( + protected workspace: Uri, + protected product: UnitTestProduct, + protected readonly serviceContainer: IServiceContainer, + cfg?: ITestConfigSettingsService, + ) { + this.installer = serviceContainer.get(IInstaller); + this.testConfigSettingsService = + cfg || serviceContainer.get(ITestConfigSettingsService); + } + + public abstract configure(wkspace: Uri): Promise; + + public abstract requiresUserToConfigure(wkspace: Uri): Promise; + + public async enable(): Promise { + // 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); + } + + public async disable(): Promise { + 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 undefined; + } + return { + label: dirName, + description: '', + }; + }) + .filter((item) => item !== undefined) + .map((item) => item!); + + items = [{ label: '.', description: 'Root directory' }, ...items]; + items = customOptions.concat(items); + return this.showQuickPick(items, options); + } + + 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'" }, + ]; + + return this.showQuickPick(items, options); + } + + 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 showQuickPick(items: QuickPickItem[], options: QuickPickOptions): Promise { + 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; + } +} diff --git a/src/client/testing/common/testUtils.ts b/src/client/testing/common/testUtils.ts index 2aa97f6950d8..04e82e1caa52 100644 --- a/src/client/testing/common/testUtils.ts +++ b/src/client/testing/common/testUtils.ts @@ -1,28 +1,10 @@ -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; +import { injectable } from 'inversify'; 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'; +import { IApplicationShell } from '../../common/application/types'; +import { Product } from '../../common/types'; +import { ITestingSettings, TestSettingsPropertyNames } from '../configuration/types'; +import { TestProvider } from '../types'; +import { ITestsHelper, UnitTestProduct } from './types'; export async function selectTestWorkspace(appShell: IApplicationShell): Promise { if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { @@ -35,31 +17,10 @@ export async function selectTestWorkspace(appShell: IApplicationShell): Promise< } } -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: @@ -71,8 +32,6 @@ export class TestsHelper implements ITestsHelper { } public parseProduct(provider: TestProvider): UnitTestProduct { switch (provider) { - case 'nosetest': - return Product.nosetest; case 'pytest': return Product.pytest; case 'unittest': @@ -89,20 +48,13 @@ export class TestsHelper implements ITestsHelper { 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 + enabledName: 'pytestEnabled' as keyof ITestingSettings, }; } case 'unittest': { return { argsName: 'unittestArgs' as keyof ITestingSettings, - enabledName: 'unittestEnabled' as keyof ITestingSettings + enabledName: 'unittestEnabled' as keyof ITestingSettings, }; } default: { @@ -110,498 +62,4 @@ export class TestsHelper implements ITestsHelper { } } } - 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 deleted file mode 100644 index bca5cc0cc271..000000000000 --- a/src/client/testing/common/testVisitors/flatteningVisitor.ts +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 5b4113bd43e5..000000000000 --- a/src/client/testing/common/testVisitors/resultResetVisitor.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 6b511f5200be..000000000000 --- a/src/client/testing/common/testVisitors/visitor.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 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 index 009afc50d311..e2fa2d6d2e5a 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -1,337 +1,83 @@ -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'; +import { CancellationToken, DebugSessionOptions, OutputChannel, Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { TestSettingsPropertyNames } from '../configuration/types'; +import { TestProvider } from '../types'; +import { PythonProject } from '../../envExt/types'; -export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; +export type UnitTestProduct = Product.pytest | Product.unittest; -export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; - -export type TestSettingsPropertyNames = { - enabledName: keyof ITestingSettings; - argsName: keyof ITestingSettings; - pathName?: keyof ITestingSettings; -}; +// **************** +// test args/options export type TestDiscoveryOptions = { workspaceFolder: Uri; cwd: string; args: string[]; - token: CancellationToken; + 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; + pytestPort?: string; + pytestUUID?: string; + runTestIdsPort?: string; + /** Optional Python project for project-based execution. */ + project?: PythonProject; }; -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 TestFilter { + removeTests = 'removeTests', + discovery = 'discovery', + runAll = 'runAll', + runSpecific = 'runSpecific', + debugAll = 'debugAll', + debugSpecific = 'debugSpecific', } -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 ITestConfigurationService = Symbol('ITestConfigurationService'); +export interface ITestConfigurationService { + hasConfiguredTests(wkspace: Uri): boolean; + selectTestRunner(placeHolderMessage: string): Promise; + enableTest(wkspace: Uri, product: UnitTestProduct): Promise; + promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise; } -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 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 const ITestResultsService = Symbol('ITestResultsService'); -export interface ITestResultsService { - resetResults(tests: Tests): void; - updateResults(tests: Tests): void; +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 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; + launchDebugger(options: LaunchOptions, callback?: () => void, sessionOptions?: DebugSessionOptions): Promise; } diff --git a/src/client/testing/common/updateTestSettings.ts b/src/client/testing/common/updateTestSettings.ts deleted file mode 100644 index dc549bdd4fae..000000000000 --- a/src/client/testing/common/updateTestSettings.ts +++ /dev/null @@ -1,155 +0,0 @@ -// 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 deleted file mode 100644 index 76ed830672ba..000000000000 --- a/src/client/testing/common/xUnitParser.ts +++ /dev/null @@ -1,183 +0,0 @@ -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 deleted file mode 100644 index 6c7adde7ede1..000000000000 --- a/src/client/testing/configuration.ts +++ /dev/null @@ -1,157 +0,0 @@ -'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/configuration/index.ts b/src/client/testing/configuration/index.ts new file mode 100644 index 000000000000..b78475293594 --- /dev/null +++ b/src/client/testing/configuration/index.ts @@ -0,0 +1,135 @@ +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { IConfigurationService, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { traceError } from '../../logging'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { TestConfiguringTelemetry } from '../../telemetry/types'; +import { BufferedTestConfigSettingsService } from '../common/bufferedTestConfigSettingService'; +import { + ITestConfigSettingsService, + ITestConfigurationManager, + ITestConfigurationManagerFactory, + ITestConfigurationService, + ITestsHelper, + UnitTestProduct, +} from '../common/types'; + +export const NONE_SELECTED = Error('none selected'); + +@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 hasConfiguredTests(wkspace: Uri): boolean { + const settings = this.configurationService.getSettings(wkspace); + return settings.testing.pytestEnabled || settings.testing.unittestEnabled || false; + } + + 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', + + detail: 'http://docs.pytest.org/', + }, + ]; + const options = { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: placeHolderMessage, + }; + const selectedTestRunner = await this.appShell.showQuickPick(items, options); + + 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): Promise { + 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( + () => configMgr.enable(), + (reason) => configMgr.enable().then(() => Promise.reject(reason)), + ); + } + + private async _promptToEnableAndConfigureTestFramework( + wkspace: Uri, + messageToDisplay = 'Select a test framework/tool to enable', + enableOnly = false, + trigger: 'ui' | 'commandpalette' = 'ui', + ): Promise { + const telemetryProps: TestConfiguringTelemetry = { + trigger, + failed: false, + }; + try { + const selectedTestRunner = await this.selectTestRunner(messageToDisplay); + if (typeof selectedTestRunner !== 'number') { + throw NONE_SELECTED; + } + const helper = this.serviceContainer.get(ITestsHelper); + telemetryProps.tool = helper.parseProviderName(selectedTestRunner); + 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) => 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/configuration/pytest/testConfigurationManager.ts b/src/client/testing/configuration/pytest/testConfigurationManager.ts new file mode 100644 index 000000000000..08f88f8564c7 --- /dev/null +++ b/src/client/testing/configuration/pytest/testConfigurationManager.ts @@ -0,0 +1,78 @@ +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 { IApplicationShell } from '../../../common/application/types'; +import { TestConfigurationManager } from '../../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../../common/types'; +import { PytestInstallationHelper } from '../pytestInstallationHelper'; +import { traceInfo } from '../../../logging'; + +export class ConfigurationManager extends TestConfigurationManager { + private readonly pytestInstallationHelper: PytestInstallationHelper; + + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { + super(workspace, Product.pytest, serviceContainer, cfg); + const appShell = serviceContainer.get(IApplicationShell); + this.pytestInstallationHelper = new PytestInstallationHelper(appShell); + } + + 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): Promise { + 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); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); + if (!installed) { + // Check if Python Environments extension is available for enhanced installation flow + if (this.pytestInstallationHelper.isEnvExtensionAvailable()) { + traceInfo('pytest not installed, prompting user with environment extension integration'); + const installAttempted = await this.pytestInstallationHelper.promptToInstallPytest(wkspace); + if (!installAttempted) { + // User chose to ignore or installation failed + return; + } + } else { + // Fall back to traditional installer + traceInfo('pytest not installed, falling back to traditional installer'); + await this.installer.install(Product.pytest); + } + } + } + + 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/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts new file mode 100644 index 000000000000..bd5fbcd5bb37 --- /dev/null +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, l10n } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceInfo, traceError } from '../../logging'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { getEnvironment } from '../../envExt/api.internal'; + +/** + * Helper class to handle pytest installation using the appropriate method + * based on whether the Python Environments extension is available. + */ +export class PytestInstallationHelper { + constructor(private readonly appShell: IApplicationShell) {} + + /** + * Prompts the user to install pytest with appropriate installation method. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was attempted, false otherwise + */ + async promptToInstallPytest(workspaceUri: Uri): Promise { + const message = l10n.t('pytest selected but not installed. Would you like to install pytest?'); + const installOption = l10n.t('Install pytest'); + + const selection = await this.appShell.showInformationMessage(message, { modal: true }, installOption); + + if (selection === installOption) { + return this.installPytest(workspaceUri); + } + + return false; + } + + /** + * Installs pytest using the appropriate method based on available extensions. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytest(workspaceUri: Uri): Promise { + try { + if (useEnvExtension()) { + return this.installPytestWithEnvExtension(workspaceUri); + } else { + // Fall back to traditional installer if environments extension is not available + traceInfo( + 'Python Environments extension not available, installation cannot proceed via environment extension', + ); + return false; + } + } catch (error) { + traceError('Error installing pytest:', error); + return false; + } + } + + /** + * Installs pytest using the Python Environments extension. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytestWithEnvExtension(workspaceUri: Uri): Promise { + try { + const envExtApi = await getEnvExtApi(); + const environment = await getEnvironment(workspaceUri); + + if (!environment) { + traceError('No Python environment found for workspace:', workspaceUri.fsPath); + await this.appShell.showErrorMessage( + l10n.t('No Python environment found. Please set up a Python environment first.'), + ); + return false; + } + + traceInfo('Installing pytest using Python Environments extension...'); + await envExtApi.managePackages(environment, { + install: ['pytest'], + }); + + traceInfo('pytest installation completed successfully'); + return true; + } catch (error) { + traceError('Failed to install pytest using Python Environments extension:', error); + return false; + } + } + + /** + * Checks if the Python Environments extension is available for package management. + * @returns True if the extension is available, false otherwise + */ + isEnvExtensionAvailable(): boolean { + return useEnvExtension(); + } +} diff --git a/src/client/testing/configuration/types.ts b/src/client/testing/configuration/types.ts new file mode 100644 index 000000000000..3b759bcb39e8 --- /dev/null +++ b/src/client/testing/configuration/types.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface ITestingSettings { + readonly promptToConfigure: boolean; + readonly debugPort: number; + readonly pytestEnabled: boolean; + pytestPath: string; + pytestArgs: string[]; + readonly unittestEnabled: boolean; + unittestArgs: string[]; + cwd?: string; + readonly autoTestDiscoverOnSaveEnabled: boolean; + readonly autoTestDiscoverOnSavePattern: string; +} + +export type TestSettingsPropertyNames = { + enabledName: keyof ITestingSettings; + argsName: keyof ITestingSettings; + pathName?: keyof ITestingSettings; +}; diff --git a/src/client/testing/configuration/unittest/testConfigurationManager.ts b/src/client/testing/configuration/unittest/testConfigurationManager.ts new file mode 100644 index 000000000000..b1482c2a42bc --- /dev/null +++ b/src/client/testing/configuration/unittest/testConfigurationManager.ts @@ -0,0 +1,37 @@ +import { Uri } from 'vscode'; +import { Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { TestConfigurationManager } from '../../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../../common/types'; + +export class ConfigurationManager extends TestConfigurationManager { + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { + super(workspace, Product.unittest, serviceContainer, cfg); + } + + // eslint-disable-next-line class-methods-use-this + public async requiresUserToConfigure(_wkspace: Uri): Promise { + return true; + } + + public async configure(wkspace: Uri): Promise { + 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/testing/configurationFactory.ts b/src/client/testing/configurationFactory.ts index 72a9065260e9..b661a38d5779 100644 --- a/src/client/testing/configurationFactory.ts +++ b/src/client/testing/configurationFactory.ts @@ -7,10 +7,13 @@ 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'; +import * as pytest from './configuration/pytest/testConfigurationManager'; +import { + ITestConfigSettingsService, + ITestConfigurationManager, + ITestConfigurationManagerFactory, +} from './common/types'; +import * as unittest from './configuration/unittest/testConfigurationManager'; @injectable() export class TestConfigurationManagerFactory implements ITestConfigurationManagerFactory { @@ -23,9 +26,6 @@ export class TestConfigurationManagerFactory implements ITestConfigurationManage 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 deleted file mode 100644 index 319ec5d2e3f9..000000000000 --- a/src/client/testing/display/main.ts +++ /dev/null @@ -1,224 +0,0 @@ -'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 deleted file mode 100644 index 7ad8657d337f..000000000000 --- a/src/client/testing/display/picker.ts +++ /dev/null @@ -1,353 +0,0 @@ -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 deleted file mode 100644 index 2dd96039c491..000000000000 --- a/src/client/testing/explorer/commandHandlers.ts +++ /dev/null @@ -1,97 +0,0 @@ -// 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 deleted file mode 100644 index ee2ae7a3ba7b..000000000000 --- a/src/client/testing/explorer/failedTestHandler.ts +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index 53fdd74eb75b..000000000000 --- a/src/client/testing/explorer/testTreeViewItem.ts +++ /dev/null @@ -1,160 +0,0 @@ -// 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(); - } - public get contextValue(): string { - return this.testType; - } - - 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); - } - } - } - - 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 deleted file mode 100644 index aa5aa4405f5a..000000000000 --- a/src/client/testing/explorer/testTreeViewProvider.ts +++ /dev/null @@ -1,210 +0,0 @@ -// 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 deleted file mode 100644 index 0c683ca307db..000000000000 --- a/src/client/testing/explorer/treeView.ts +++ /dev/null @@ -1,45 +0,0 @@ -// 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 index f4763bd74b23..eed4d70e852c 100644 --- a/src/client/testing/main.ts +++ b/src/client/testing/main.ts @@ -1,621 +1,230 @@ '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 + Uri, + tests, + TestResultState, + WorkspaceFolder, + Command, + TestItem, } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { IApplicationShell, ICommandManager, IContextKeyManager, 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 { IDisposableRegistry, Product } from '../common/types'; 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 { sendTelemetryEvent } from '../telemetry/index'; 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 +import { TestSettingsPropertyNames } from './configuration/types'; +import { ITestConfigurationService, ITestsHelper } from './common/types'; +import { ITestingService } from './types'; +import { IExtensionActivationService } from '../activation/types'; +import { ITestController } from './testController/common/types'; +import { DelayedTrigger, IDelayedTrigger } from '../common/utils/delayTrigger'; +import { ExtensionContextKey } from '../common/application/contextKeys'; +import { checkForFailedTests, updateTestResultMap } from './testController/common/testItemUtilities'; +import { Testing } from '../common/utils/localize'; +import { traceVerbose, traceWarn } from '../logging'; +import { writeTestIdToClipboard } from './utils'; @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); +export class TestingService implements ITestingService { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - 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 getSettingsPropertyNames(product: Product): TestSettingsPropertyNames { + const helper = this.serviceContainer.get(ITestsHelper); + return helper.getSettingsPropertyNames(product); } - public async activate(symbolProvider: DocumentSymbolProvider): Promise { - if (this.activatedOnce) { - return; - } - this.activatedOnce = true; - this.workspaceTestManagerService = this.serviceContainer.get( - IWorkspaceTestManagerService - ); +} + +/** + * Registers command handlers but defers service resolution until the commands are actually invoked, + * allowing registration to happen before all services are fully initialized. + */ +export function registerTestCommands(serviceContainer: IServiceContainer): void { + // Resolve only the essential services needed for command registration itself + const disposableRegistry = serviceContainer.get(IDisposableRegistry); + const commandManager = serviceContainer.get(ICommandManager); + + // Helper function to configure tests - services are resolved when invoked, not at registration time + const configureTestsHandler = async (resource?: Uri) => { + sendTelemetryEvent(EventName.UNITTEST_CONFIGURE); + + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); - 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); + const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; } else { - const appShell = this.serviceContainer.get(IApplicationShell); + const appShell = 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(); + const interpreterService = serviceContainer.get(IInterpreterService); + const cmdManager = serviceContainer.get(ICommandManager); + if (!(await interpreterService.getActiveInterpreter(wkspace))) { + cmdManager.executeCommand(constants.Commands.TriggerEnvironmentSelection, wkspace); + return; + } + const configurationService = serviceContainer.get(ITestConfigurationService); + await configurationService.promptToEnableAndConfigureTestFramework(wkspace); + }; + + disposableRegistry.push( + // Command: python.configureTests - prompts user to configure test framework + commandManager.registerCommand( + constants.Commands.Tests_Configure, + (_, _cmdSource: constants.CommandSource = constants.CommandSource.commandPalette, resource?: Uri) => { + // Invoke configuration handler (errors are ignored as this can be called from multiple places) + configureTestsHandler(resource).ignoreErrors(); + traceVerbose('Testing: Trigger refresh after config change'); + // Refresh test data if test controller is available (resolved lazily) + if (tests && !!tests.createTestController) { + const testController = serviceContainer.get(ITestController); + testController?.refreshTestData(resource, { forceRefresh: true }); + } + }, + ), + // Command: python.tests.copilotSetup - Copilot integration for test setup + commandManager.registerCommand(constants.Commands.Tests_CopilotSetup, (resource?: Uri): + | { message: string; command: Command } + | undefined => { + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); + const wkspaceFolder = + workspaceService.getWorkspaceFolder(resource) || workspaceService.workspaceFolders?.at(0); + if (!wkspaceFolder) { + return undefined; } - 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; - } + const configurationService = serviceContainer.get(ITestConfigurationService); + if (configurationService.hasConfiguredTests(wkspaceFolder.uri)) { + return undefined; + } - 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; - } + return { + message: Testing.copilotSetupMessage, + command: { + title: Testing.configureTests, + command: constants.Commands.Tests_Configure, + arguments: [undefined, constants.CommandSource.ui, resource], + }, + }; + }), + // Command: python.copyTestId - copies test ID to clipboard + commandManager.registerCommand(constants.Commands.CopyTestId, async (testItem: TestItem) => { + writeTestIdToClipboard(testItem); + }), + ); +} - if (testManager.status === TestStatus.Discovering || testManager.status === TestStatus.Running) { - return; - } +@injectable() +export class UnitTestManagementService implements IExtensionActivationService { + private activatedOnce: boolean = false; + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + private readonly disposableRegistry: Disposable[]; + private workspaceService: IWorkspaceService; + private context: IContextKeyManager; + private testController: ITestController | undefined; + private configChangeTrigger: IDelayedTrigger; - 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; - } + // This is temporarily needed until the proposed API settles for this part + private testStateMap: Map = new Map(); - 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; - } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.disposableRegistry = serviceContainer.get(IDisposableRegistry); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.context = this.serviceContainer.get(IContextKeyManager); - 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; + if (tests && !!tests.createTestController) { + this.testController = serviceContainer.get(ITestController); } - const testDisplay = this.serviceContainer.get(ITestDisplay); - testDisplay.displayFunctionTestPickerUI( - cmdSource, - testManager.workspaceFolder, - testManager.workingDirectory, - file, - testFunctions, - debug + const configChangeTrigger = new DelayedTrigger( + this.configurationChangeHandler.bind(this), + 500, + 'Test Configuration Change', ); + this.configChangeTrigger = configChangeTrigger; + this.disposableRegistry.push(configChangeTrigger); } - 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) { + public async activate(): Promise { + if (this.activatedOnce) { return; } + this.activatedOnce = true; - 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]] }); - } + this.registerHandlers(); - 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 (!!tests.testResults) { + await this.updateTestUIButtons(); + this.disposableRegistry.push( + tests.onDidChangeTestResults(() => { + this.updateTestUIButtons(); + }), + ); } - if (!this.testResultDisplay) { - this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + if (this.testController) { + this.testController.onRefreshingStarted(async () => { + await this.context.setContext(ExtensionContextKey.RefreshingTests, true); + }); + this.testController.onRefreshingCompleted(async () => { + await this.context.setContext(ExtensionContextKey.RefreshingTests, false); + }); + this.testController.onRunWithoutConfiguration(async (unconfigured: WorkspaceFolder[]) => { + const workspaces = this.workspaceService.workspaceFolders ?? []; + if (unconfigured.length === workspaces.length) { + const commandManager = this.serviceContainer.get(ICommandManager); + await commandManager.executeCommand('workbench.view.testing.focus'); + traceWarn( + 'Testing: Run attempted but no test configurations found for any workspace, use command palette to configure tests for python if desired.', + ); + } + }); } - - 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) - ); - } + private async updateTestUIButtons() { + // See if we already have stored tests results from previous runs. + // The tests results currently has a historical test status based on runs. To get a + // full picture of the tests state these need to be reduced by test id. + updateTestResultMap(this.testStateMap, tests.testResults); - @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!); + const hasFailedTests = checkForFailedTests(this.testStateMap); + await this.context.setContext(ExtensionContextKey.HasFailedTests, hasFailedTests); } - // 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) - ]; + private async configurationChangeHandler(eventArgs: ConfigurationChangeEvent) { + const workspaces = this.workspaceService.workspaceFolders ?? []; + const changedWorkspaces: Uri[] = workspaces + .filter((w) => eventArgs.affectsConfiguration('python.testing', w.uri)) + .map((w) => w.uri); - disposablesRegistry.push(...disposables); + await Promise.all(changedWorkspaces.map((u) => this.testController?.refreshTestData(u))); } - 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))); + private registerHandlers() { + const interpreterService = this.serviceContainer.get(IInterpreterService); 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) - ) - ) + this.configChangeTrigger.trigger(e); + }), + interpreterService.onDidChangeInterpreter(async () => { + traceVerbose('Testing: Triggered refresh due to interpreter change.'); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'interpreter' }); + await this.testController?.refreshTestData(undefined, { forceRefresh: true }); + }), ); } } diff --git a/src/client/testing/navigation/commandHandler.ts b/src/client/testing/navigation/commandHandler.ts deleted file mode 100644 index 27df132391bb..000000000000 --- a/src/client/testing/navigation/commandHandler.ts +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index 12f5ac69f8ab..000000000000 --- a/src/client/testing/navigation/fileNavigator.ts +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index 0c95e42128b6..000000000000 --- a/src/client/testing/navigation/functionNavigator.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index 7c5e092ea4a7..000000000000 --- a/src/client/testing/navigation/helper.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index bccf480a5b9f..000000000000 --- a/src/client/testing/navigation/serviceRegistry.ts +++ /dev/null @@ -1,38 +0,0 @@ -// 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 deleted file mode 100644 index 0230daa95e56..000000000000 --- a/src/client/testing/navigation/suiteNavigator.ts +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index 806776d89245..000000000000 --- a/src/client/testing/navigation/symbolProvider.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index 62b9e4b809c4..000000000000 --- a/src/client/testing/navigation/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index e8bccbf26bf9..000000000000 --- a/src/client/testing/nosetest/main.ts +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index e29a95e155ce..000000000000 --- a/src/client/testing/nosetest/runner.ts +++ /dev/null @@ -1,124 +0,0 @@ -'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 deleted file mode 100644 index cba04ef343b2..000000000000 --- a/src/client/testing/nosetest/services/argsService.ts +++ /dev/null @@ -1,205 +0,0 @@ -// 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 deleted file mode 100644 index 616b40d14f6c..000000000000 --- a/src/client/testing/nosetest/services/discoveryService.ts +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index 80fb184dfc97..000000000000 --- a/src/client/testing/nosetest/services/parserService.ts +++ /dev/null @@ -1,189 +0,0 @@ -// 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 deleted file mode 100644 index 3873db1c5b1b..000000000000 --- a/src/client/testing/pytest/main.ts +++ /dev/null @@ -1,89 +0,0 @@ -'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 deleted file mode 100644 index cde8e4765125..000000000000 --- a/src/client/testing/pytest/runner.ts +++ /dev/null @@ -1,120 +0,0 @@ -'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 deleted file mode 100644 index 8223841f46fc..000000000000 --- a/src/client/testing/pytest/services/argsService.ts +++ /dev/null @@ -1,282 +0,0 @@ -// 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-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', - '--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', - '-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 deleted file mode 100644 index 965336217405..000000000000 --- a/src/client/testing/pytest/services/discoveryService.ts +++ /dev/null @@ -1,69 +0,0 @@ -// 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 deleted file mode 100644 index 049833c001e9..000000000000 --- a/src/client/testing/pytest/services/testMessageService.ts +++ /dev/null @@ -1,279 +0,0 @@ -// 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 deleted file mode 100644 index 2c3b627342a1..000000000000 --- a/src/client/testing/pytest/testConfigurationManager.ts +++ /dev/null @@ -1,56 +0,0 @@ -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 index 5ebb8dc4f36f..d36fab7686f8 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -1,190 +1,38 @@ // 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 { IExtensionActivationService } from '../activation/types'; +import { IServiceManager } from '../ioc/types'; 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 { TestConfigSettingsService } from './common/configSettingService'; import { TestsHelper } from './common/testUtils'; -import { TestFlatteningVisitor } from './common/testVisitors/flatteningVisitor'; -import { TestResultResetVisitor } from './common/testVisitors/resultResetVisitor'; import { - ITestCollectionStorageService, - ITestContextService, + ITestConfigSettingsService, + ITestConfigurationManagerFactory, + ITestConfigurationService, 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'; +import { TestingService, UnitTestManagementService } from './main'; +import { ITestingService } from './types'; +import { registerTestControllerTypes } from './testController/serviceRegistry'; 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(ITestingService, TestingService); + serviceManager.addSingleton(ITestConfigSettingsService, TestConfigSettingsService); serviceManager.addSingleton( ITestConfigurationManagerFactory, - TestConfigurationManagerFactory + TestConfigurationManagerFactory, ); + serviceManager.addSingleton(IExtensionActivationService, UnitTestManagementService); - 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); - }; - }); + registerTestControllerTypes(serviceManager); } diff --git a/src/client/testing/testController/common/argumentsHelper.ts b/src/client/testing/testController/common/argumentsHelper.ts new file mode 100644 index 000000000000..c155d0197da7 --- /dev/null +++ b/src/client/testing/testController/common/argumentsHelper.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceWarn } from '../../../logging'; + +export function getPositionalArguments( + args: string[], + optionsWithArguments: string[] = [], + optionsWithoutArguments: string[] = [], +): string[] { + const nonPositionalIndexes: number[] = []; + args.forEach((arg, index) => { + if (optionsWithoutArguments.indexOf(arg) !== -1) { + nonPositionalIndexes.push(index); + } 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); + } else if (arg.startsWith('-')) { + // Ok this is an unknown option, lets treat this as one without values. + traceWarn( + `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); + } else if (arg.indexOf('=') > 0) { + // Ok this is an unknown option with a value + traceWarn( + `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); +} + +export function filterArguments( + args: string[], + optionsWithArguments: string[] = [], + optionsWithoutArguments: string[] = [], +): string[] { + let ignoreIndex = -1; + return args.filter((arg, index) => { + if (ignoreIndex === index) { + return false; + } + // Options can use wild 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 inline 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 inline 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/testController/common/discoveryHelpers.ts b/src/client/testing/testController/common/discoveryHelpers.ts new file mode 100644 index 000000000000..e170ad576ae8 --- /dev/null +++ b/src/client/testing/testController/common/discoveryHelpers.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationToken, CancellationTokenSource, Disposable, Uri } from 'vscode'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { createDiscoveryErrorPayload, fixLogLinesNoTrailing, startDiscoveryNamedPipe } from './utils'; +import { DiscoveredTestPayload, ITestResultResolver } from './types'; + +/** + * Test provider type for logging purposes. + */ +export type TestProvider = 'pytest' | 'unittest'; + +/** + * Sets up the discovery named pipe and wires up cancellation. + * @param resultResolver The resolver to handle discovered test data + * @param token Optional cancellation token from the caller + * @param uri Workspace URI for logging + * @returns Object containing the pipe name, cancellation source, and disposable for the external token handler + */ +export async function setupDiscoveryPipe( + resultResolver: ITestResultResolver | undefined, + token: CancellationToken | undefined, + uri: Uri, +): Promise<{ pipeName: string; cancellation: CancellationTokenSource; tokenDisposable: Disposable | undefined }> { + const discoveryPipeCancellation = new CancellationTokenSource(); + + // Wire up cancellation from external token and store the disposable + const tokenDisposable = token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled.`); + discoveryPipeCancellation.cancel(); + }); + + // Start the named pipe with the discovery listener + const discoveryPipeName = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + if (!token?.isCancellationRequested) { + resultResolver?.resolveDiscovery(data); + } + }, discoveryPipeCancellation.token); + + traceVerbose(`Created discovery pipe: ${discoveryPipeName} for workspace ${uri.fsPath}`); + + return { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + }; +} + +/** + * Creates standard process event handlers for test discovery subprocess. + * Handles stdout/stderr logging and error reporting on process exit. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param uri - The workspace URI + * @param cwd - The current working directory + * @param resultResolver - Resolver for test discovery results + * @param deferredTillExecClose - Deferred to resolve when process closes + * @param allowedSuccessCodes - Additional exit codes to treat as success (e.g., pytest exit code 5 for no tests found) + */ +export function createProcessHandlers( + testProvider: TestProvider, + uri: Uri, + cwd: string, + resultResolver: ITestResultResolver | undefined, + deferredTillExecClose: Deferred, + allowedSuccessCodes: number[] = [], +): { + onStdout: (data: any) => void; + onStderr: (data: any) => void; + onExit: (code: number | null, signal: NodeJS.Signals | null) => void; + onClose: (code: number | null, signal: NodeJS.Signals | null) => void; +} { + const isSuccessCode = (code: number | null): boolean => { + return code === 0 || (code !== null && allowedSuccessCodes.includes(code)); + }; + + return { + onStdout: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceInfo(out); + }, + onStderr: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceError(out); + }, + onExit: (code: number | null, _signal: NodeJS.Signals | null) => { + // The 'exit' event fires when the process terminates, but streams may still be open. + // Only log verbose success message here; error handling happens in onClose. + if (isSuccessCode(code)) { + traceVerbose(`${testProvider} discovery subprocess exited successfully for workspace ${uri.fsPath}`); + } + }, + onClose: (code: number | null, signal: NodeJS.Signals | null) => { + // We resolve the deferred here to ensure all output has been captured. + if (!isSuccessCode(code)) { + traceError( + `${testProvider} discovery failed with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating error payload.`, + ); + resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd)); + } else { + traceVerbose(`${testProvider} discovery subprocess streams closed for workspace ${uri.fsPath}`); + } + deferredTillExecClose?.resolve(); + }, + }; +} + +/** + * Handles cleanup when test discovery is cancelled. + * Kills the subprocess (if running), resolves the completion deferred, and cancels the discovery pipe. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param proc - The process to kill + * @param processCompletion - Deferred to resolve + * @param pipeCancellation - Cancellation token source to cancel + * @param uri - The workspace URI + */ +export function cleanupOnCancellation( + testProvider: TestProvider, + proc: { kill: () => void } | undefined, + processCompletion: Deferred, + pipeCancellation: CancellationTokenSource, + uri: Uri, +): void { + traceInfo(`Test discovery cancelled, killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + if (proc) { + traceVerbose(`Killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + proc.kill(); + } else { + traceVerbose(`No ${testProvider} subprocess to kill for workspace ${uri.fsPath} (proc is undefined)`); + } + traceVerbose(`Resolving process completion deferred for ${testProvider} discovery in workspace ${uri.fsPath}`); + processCompletion.resolve(); + traceVerbose(`Cancelling discovery pipe for ${testProvider} discovery in workspace ${uri.fsPath}`); + pipeCancellation.cancel(); +} diff --git a/src/client/testing/testController/common/projectAdapter.ts b/src/client/testing/testController/common/projectAdapter.ts new file mode 100644 index 000000000000..cfffbf439ca6 --- /dev/null +++ b/src/client/testing/testController/common/projectAdapter.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestItem, Uri } from 'vscode'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { PythonEnvironment, PythonProject } from '../../../envExt/types'; + +/** + * Represents a single Python project with its own test infrastructure. + * A project is defined as a combination of a Python executable + URI (folder/file). + * Projects are uniquely identified by their projectUri (use projectUri.toString() for map keys). + */ +export interface ProjectAdapter { + // === IDENTITY === + /** + * Display name for the project (e.g., "alice (Python 3.11)"). + */ + projectName: string; + + /** + * URI of the project root folder or file. + * This is the unique identifier for the project. + */ + projectUri: Uri; + + /** + * Parent workspace URI containing this project. + */ + workspaceUri: Uri; + + // === API OBJECTS (from vscode-python-environments extension) === + /** + * The PythonProject object from the environment API. + */ + pythonProject: PythonProject; + + /** + * The resolved PythonEnvironment with execution details. + * Contains execInfo.run.executable for running tests. + */ + pythonEnvironment: PythonEnvironment; + + // === TEST INFRASTRUCTURE === + /** + * Test framework provider ('pytest' | 'unittest'). + */ + testProvider: TestProvider; + + /** + * Adapter for test discovery. + */ + discoveryAdapter: ITestDiscoveryAdapter; + + /** + * Adapter for test execution. + */ + executionAdapter: ITestExecutionAdapter; + + /** + * Result resolver for this project (maps test IDs and handles results). + */ + resultResolver: ITestResultResolver; + + /** + * Absolute paths of nested projects to ignore during discovery. + * Used to pass --ignore flags to pytest or exclusion filters to unittest. + * Only populated for parent projects that contain nested child projects. + */ + nestedProjectPathsToIgnore?: string[]; + + // === LIFECYCLE === + /** + * Whether discovery is currently running for this project. + */ + isDiscovering: boolean; + + /** + * Whether tests are currently executing for this project. + */ + isExecuting: boolean; + + /** + * Root TestItem for this project in the VS Code test tree. + * All project tests are children of this item. + */ + projectRootTestItem?: TestItem; +} diff --git a/src/client/testing/testController/common/projectTestExecution.ts b/src/client/testing/testController/common/projectTestExecution.ts new file mode 100644 index 000000000000..fe3b4f91491a --- /dev/null +++ b/src/client/testing/testController/common/projectTestExecution.ts @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, FileCoverageDetail, TestItem, TestRun, TestRunProfileKind, TestRunRequest } from 'vscode'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { ITestDebugLauncher } from '../../common/types'; +import { ProjectAdapter } from './projectAdapter'; +import { TestProjectRegistry } from './testProjectRegistry'; +import { getProjectId } from './projectUtils'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { isParentPath } from '../../../pythonEnvironments/common/externalDependencies'; + +/** Dependencies for project-based test execution. */ +export interface ProjectExecutionDependencies { + projectRegistry: TestProjectRegistry; + pythonExecFactory: IPythonExecutionFactory; + debugLauncher: ITestDebugLauncher; +} + +/** Executes tests for multiple projects, grouping by project and using each project's Python environment. */ +export async function executeTestsForProjects( + projects: ProjectAdapter[], + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + deps: ProjectExecutionDependencies, +): Promise { + if (projects.length === 0) { + traceError(`[test-by-project] No projects provided for execution`); + return; + } + + // Early exit if already cancelled + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Execution cancelled before starting`); + return; + } + + // Group test items by project + const testsByProject = await groupTestItemsByProject(testItems, projects); + + const isDebugMode = request.profile?.kind === TestRunProfileKind.Debug; + traceInfo(`[test-by-project] Executing tests across ${testsByProject.size} project(s), debug=${isDebugMode}`); + + // Setup coverage once for all projects (single callback that routes by file path) + if (request.profile?.kind === TestRunProfileKind.Coverage) { + setupCoverageForProjects(request, projects); + } + + // Execute tests for each project in parallel + // For debug mode, multiple debug sessions will be launched in parallel + // Each execution respects cancellation via runInstance.token + const executions = Array.from(testsByProject.entries()).map(async ([_projectId, { project, items }]) => { + // Check for cancellation before starting each project + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Skipping ${project.projectName} - cancellation requested`); + return; + } + + if (items.length === 0) return; + + traceInfo(`[test-by-project] Executing ${items.length} test item(s) for project: ${project.projectName}`); + + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: project.testProvider, + debugging: isDebugMode, + }); + + try { + await executeTestsForProject(project, items, runInstance, request, deps); + } catch (error) { + // Don't log cancellation as an error + if (!token.isCancellationRequested) { + traceError(`[test-by-project] Execution failed for project ${project.projectName}:`, error); + } + } + }); + + await Promise.all(executions); + + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Project executions cancelled`); + } else { + traceInfo(`[test-by-project] All project executions completed`); + } +} + +/** Lookup context for caching project lookups within a single test run. */ +interface ProjectLookupContext { + uriToAdapter: Map; + projectPathToAdapter: Map; +} + +/** Groups test items by owning project using env API or path-based matching as fallback. */ +export async function groupTestItemsByProject( + testItems: TestItem[], + projects: ProjectAdapter[], +): Promise> { + const result = new Map(); + + // Initialize entries for all projects + for (const project of projects) { + result.set(getProjectId(project.projectUri), { project, items: [] }); + } + + // Build lookup context for this run - O(p) one-time setup, enables O(1) lookups per item. + // When tests are from a single project, most lookups hit the cache after the first item. + const lookupContext: ProjectLookupContext = { + uriToAdapter: new Map(), + projectPathToAdapter: new Map(projects.map((p) => [p.projectUri.fsPath, p])), + }; + + // Assign each test item to its project + for (const item of testItems) { + const project = await findProjectForTestItem(item, projects, lookupContext); + if (project) { + const entry = result.get(getProjectId(project.projectUri)); + if (entry) { + entry.items.push(item); + } + } else { + // If no project matches, log it + traceWarn(`[test-by-project] Could not match test item ${item.id} to a project`); + } + } + + // Remove projects with no test items + for (const [projectId, entry] of result.entries()) { + if (entry.items.length === 0) { + result.delete(projectId); + } + } + + return result; +} + +/** Finds the project that owns a test item. */ +export async function findProjectForTestItem( + item: TestItem, + projects: ProjectAdapter[], + lookupContext?: ProjectLookupContext, +): Promise { + if (!item.uri) return undefined; + + const uriPath = item.uri.fsPath; + + // Check lookup context first - O(1) + if (lookupContext?.uriToAdapter.has(uriPath)) { + return lookupContext.uriToAdapter.get(uriPath); + } + + let result: ProjectAdapter | undefined; + + // Try using the Python Environment extension API first. + // Legacy path: when useEnvExtension() is false, this block is skipped and we go + // directly to findProjectByPath() below (path-based matching). + if (useEnvExtension()) { + try { + const envExtApi = await getEnvExtApi(); + const pythonProject = envExtApi.getPythonProject(item.uri); + if (pythonProject) { + // Use lookup context for O(1) adapter lookup instead of O(p) linear search + result = lookupContext?.projectPathToAdapter.get(pythonProject.uri.fsPath); + if (!result) { + // Fallback to linear search if lookup context not available + result = projects.find((p) => p.projectUri.fsPath === pythonProject.uri.fsPath); + } + } + } catch (error) { + traceVerbose(`[test-by-project] Failed to use env extension API, falling back to path matching: ${error}`); + } + } + + // Fallback: path-based matching when env API unavailable or didn't find a match. + // O(p) time complexity where p = number of projects. + if (!result) { + result = findProjectByPath(item, projects); + } + + // Store result for future lookups of same file within this run - O(1) + if (lookupContext) { + lookupContext.uriToAdapter.set(uriPath, result); + } + + return result; +} + +/** Fallback: finds project using path-based matching. */ +function findProjectByPath(item: TestItem, projects: ProjectAdapter[]): ProjectAdapter | undefined { + if (!item.uri) return undefined; + + const itemPath = item.uri.fsPath; + let bestMatch: ProjectAdapter | undefined; + let bestMatchLength = 0; + + for (const project of projects) { + const projectPath = project.projectUri.fsPath; + // Use isParentPath for safe path-boundary matching (handles separators and case normalization) + if (isParentPath(itemPath, projectPath) && projectPath.length > bestMatchLength) { + bestMatch = project; + bestMatchLength = projectPath.length; + } + } + + return bestMatch; +} + +/** Executes tests for a single project using the project's Python environment. */ +export async function executeTestsForProject( + project: ProjectAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + deps: ProjectExecutionDependencies, +): Promise { + const processedTestItemIds = new Set(); + const uniqueTestCaseIds = new Set(); + + // Mark items as started and collect test IDs (deduplicated to handle overlapping selections) + for (const item of testItems) { + const testCaseNodes = getTestCaseNodesRecursive(item); + for (const node of testCaseNodes) { + if (processedTestItemIds.has(node.id)) { + continue; + } + processedTestItemIds.add(node.id); + runInstance.started(node); + const runId = project.resultResolver.vsIdToRunId.get(node.id); + if (runId) { + uniqueTestCaseIds.add(runId); + } + } + } + + const testCaseIds = Array.from(uniqueTestCaseIds); + + if (testCaseIds.length === 0) { + traceVerbose(`[test-by-project] No test IDs found for project ${project.projectName}`); + return; + } + + traceInfo(`[test-by-project] Running ${testCaseIds.length} test(s) for project: ${project.projectName}`); + + // Execute tests using the project's execution adapter + await project.executionAdapter.runTests( + project.projectUri, + testCaseIds, + request.profile?.kind, + runInstance, + deps.pythonExecFactory, + deps.debugLauncher, + undefined, // interpreter not needed, project has its own environment + project, + ); +} + +/** Recursively gets all leaf test case nodes from a test item tree. */ +export function getTestCaseNodesRecursive(item: TestItem): TestItem[] { + const results: TestItem[] = []; + if (item.children.size === 0) { + // This is a leaf node (test case) + results.push(item); + } else { + // Recursively get children + item.children.forEach((child) => { + results.push(...getTestCaseNodesRecursive(child)); + }); + } + return results; +} + +/** Sets up detailed coverage loading that routes to the correct project by file path. */ +export function setupCoverageForProjects(request: TestRunRequest, projects: ProjectAdapter[]): void { + if (request.profile?.kind === TestRunProfileKind.Coverage) { + // Create a single callback that routes to the correct project's coverage map by file path + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const filePath = fileCoverage.uri.fsPath; + // Find the project that has coverage data for this file + for (const project of projects) { + const details = project.resultResolver.detailedCoverageMap.get(filePath); + if (details) { + return Promise.resolve(details); + } + } + return Promise.resolve([]); + }; + } +} diff --git a/src/client/testing/testController/common/projectUtils.ts b/src/client/testing/testController/common/projectUtils.ts new file mode 100644 index 000000000000..b104b7f6842d --- /dev/null +++ b/src/client/testing/testController/common/projectUtils.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { UnittestTestDiscoveryAdapter } from '../unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../unittest/testExecutionAdapter'; +import { PytestTestDiscoveryAdapter } from '../pytest/pytestDiscoveryAdapter'; +import { PytestTestExecutionAdapter } from '../pytest/pytestExecutionAdapter'; + +/** + * Separator used to scope test IDs to a specific project. + * Format: {projectId}{SEPARATOR}{testPath} + * Example: "file:///workspace/project@@PROJECT@@test_file.py::test_name" + */ +export const PROJECT_ID_SEPARATOR = '@@vsc@@'; + +/** + * Gets the project ID from a project URI. + * The project ID is simply the string representation of the URI, matching how + * the Python Environments extension stores projects in Map. + * + * @param projectUri The project URI + * @returns The project ID (URI as string) + */ +export function getProjectId(projectUri: Uri): string { + return projectUri.toString(); +} + +/** + * Parses a project-scoped vsId back into its components. + * + * @param vsId The VS Code test item ID to parse + * @returns A tuple of [projectId, runId]. If the ID is not project-scoped, + * returns [undefined, vsId] (legacy format) + */ +export function parseVsId(vsId: string): [string | undefined, string] { + const separatorIndex = vsId.indexOf(PROJECT_ID_SEPARATOR); + if (separatorIndex === -1) { + return [undefined, vsId]; // Legacy ID without project scope + } + return [vsId.substring(0, separatorIndex), vsId.substring(separatorIndex + PROJECT_ID_SEPARATOR.length)]; +} + +/** + * Creates a display name for a project including Python version. + * Format: "{projectName} (Python {version})" + * + * @param projectName The name of the project + * @param pythonVersion The Python version string (e.g., "3.11.2") + * @returns Formatted display name + */ +export function createProjectDisplayName(projectName: string, pythonVersion: string): string { + // Extract major.minor version if full version provided + const versionMatch = pythonVersion.match(/^(\d+\.\d+)/); + const shortVersion = versionMatch ? versionMatch[1] : pythonVersion; + + return `${projectName} (Python ${shortVersion})`; +} + +/** + * Creates test adapters (discovery and execution) for a given test provider. + * + * @param testProvider The test framework provider ('pytest' | 'unittest') + * @param resultResolver The result resolver to use for test results + * @param configSettings The configuration service + * @param envVarsService The environment variables provider + * @returns An object containing the discovery and execution adapters + */ +export function createTestAdapters( + testProvider: TestProvider, + resultResolver: ITestResultResolver, + configSettings: IConfigurationService, + envVarsService: IEnvironmentVariablesProvider, +): { discoveryAdapter: ITestDiscoveryAdapter; executionAdapter: ITestExecutionAdapter } { + if (testProvider === UNITTEST_PROVIDER) { + return { + discoveryAdapter: new UnittestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new UnittestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; + } + + return { + discoveryAdapter: new PytestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new PytestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; +} diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts new file mode 100644 index 000000000000..c126d233de1b --- /dev/null +++ b/src/client/testing/testController/common/resultResolver.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, TestItem, Uri, TestRun, FileCoverageDetail } from 'vscode'; +import { CoveragePayload, DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { TestProvider } from '../../types'; +import { traceInfo } from '../../../logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { TestItemIndex } from './testItemIndex'; +import { TestDiscoveryHandler } from './testDiscoveryHandler'; +import { TestExecutionHandler } from './testExecutionHandler'; +import { TestCoverageHandler } from './testCoverageHandler'; + +export class PythonResultResolver implements ITestResultResolver { + testController: TestController; + + testProvider: TestProvider; + + private testItemIndex: TestItemIndex; + + // Shared singleton handlers + private static discoveryHandler: TestDiscoveryHandler = new TestDiscoveryHandler(); + private static executionHandler: TestExecutionHandler = new TestExecutionHandler(); + private static coverageHandler: TestCoverageHandler = new TestCoverageHandler(); + + public detailedCoverageMap = new Map(); + + /** + * Optional project ID for scoping test IDs. + * When set, all test IDs are prefixed with `{projectId}@@vsc@@` for project-based testing. + * When undefined, uses legacy workspace-level IDs for backward compatibility. + */ + private projectId?: string; + + /** + * Optional project display name for labeling the test tree root. + * When set, the root node label will be "project: {projectName}" instead of the folder name. + */ + private projectName?: string; + + constructor( + testController: TestController, + testProvider: TestProvider, + private workspaceUri: Uri, + projectId?: string, + projectName?: string, + ) { + this.testController = testController; + this.testProvider = testProvider; + this.projectId = projectId; + this.projectName = projectName; + // Initialize a new TestItemIndex which will be used to track test items in this workspace/project + this.testItemIndex = new TestItemIndex(); + } + + // Expose for backward compatibility (WorkspaceTestAdapter accesses these) + public get runIdToTestItem(): Map { + return this.testItemIndex.runIdToTestItemMap; + } + + public get runIdToVSid(): Map { + return this.testItemIndex.runIdToVSidMap; + } + + public get vsIdToRunId(): Map { + return this.testItemIndex.vsIdToRunIdMap; + } + + /** + * Gets the project ID for this resolver (if any). + * Used for project-scoped test ID generation. + */ + public getProjectId(): string | undefined { + return this.projectId; + } + + public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + PythonResultResolver.discoveryHandler.processDiscovery( + payload, + this.testController, + this.testItemIndex, + this.workspaceUri, + this.testProvider, + token, + this.projectId, + this.projectName, + ); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { + tool: this.testProvider, + failed: false, + }); + } + + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + // Delegate to the public method for backward compatibility + this.resolveDiscovery(payload, token); + } + + public resolveExecution(payload: ExecutionTestPayload | CoveragePayload, runInstance: TestRun): void { + if ('coverage' in payload) { + // coverage data is sent once per connection + traceInfo('Coverage data received, processing...'); + this.detailedCoverageMap = PythonResultResolver.coverageHandler.processCoverage( + payload as CoveragePayload, + runInstance, + ); + traceInfo('Coverage data processing complete.'); + } else { + PythonResultResolver.executionHandler.processExecution( + payload as ExecutionTestPayload, + runInstance, + this.testItemIndex, + this.testController, + ); + } + } + + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); + } + + public _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); + } + + /** + * Clean up stale test item references from the cache maps. + * Validates cached items and removes any that are no longer in the test tree. + * Delegates to TestItemIndex. + */ + public cleanupStaleReferences(): void { + this.testItemIndex.cleanupStaleReferences(this.testController); + } +} diff --git a/src/client/testing/testController/common/testCoverageHandler.ts b/src/client/testing/testController/common/testCoverageHandler.ts new file mode 100644 index 000000000000..81ec80579730 --- /dev/null +++ b/src/client/testing/testController/common/testCoverageHandler.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, TestCoverageCount, FileCoverage, FileCoverageDetail, StatementCoverage, Range } from 'vscode'; +import { CoveragePayload, FileCoverageMetrics } from './types'; + +/** + * Stateless handler for processing coverage payloads and creating coverage objects. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestCoverageHandler { + /** + * Process coverage payload + * Pure function - returns coverage data without storing it + */ + public processCoverage(payload: CoveragePayload, runInstance: TestRun): Map { + const detailedCoverageMap = new Map(); + + if (payload.result === undefined) { + return detailedCoverageMap; + } + + for (const [key, value] of Object.entries(payload.result)) { + const fileNameStr = key; + const fileCoverageMetrics: FileCoverageMetrics = value; + + // Create FileCoverage object and add to run instance + const fileCoverage = this.createFileCoverage(Uri.file(fileNameStr), fileCoverageMetrics); + runInstance.addCoverage(fileCoverage); + + // Create detailed coverage array for this file + const detailedCoverage = this.createDetailedCoverage( + fileCoverageMetrics.lines_covered ?? [], + fileCoverageMetrics.lines_missed ?? [], + ); + detailedCoverageMap.set(Uri.file(fileNameStr).fsPath, detailedCoverage); + } + + return detailedCoverageMap; + } + + /** + * Create FileCoverage object from metrics + */ + private createFileCoverage(uri: Uri, metrics: FileCoverageMetrics): FileCoverage { + const linesCovered = metrics.lines_covered ?? []; + const linesMissed = metrics.lines_missed ?? []; + const executedBranches = metrics.executed_branches; + const totalBranches = metrics.total_branches; + + const lineCoverageCount = new TestCoverageCount(linesCovered.length, linesCovered.length + linesMissed.length); + + if (totalBranches === -1) { + // branch coverage was not enabled and should not be displayed + return new FileCoverage(uri, lineCoverageCount); + } else { + const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches); + return new FileCoverage(uri, lineCoverageCount, branchCoverageCount); + } + } + + /** + * Create detailed coverage array for a file + * Only line coverage on detailed, not branch coverage + */ + private createDetailedCoverage(linesCovered: number[], linesMissed: number[]): FileCoverageDetail[] { + const detailedCoverageArray: FileCoverageDetail[] = []; + + // Add covered lines + for (const line of linesCovered) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // true value means line is covered + const statementCoverage = new StatementCoverage( + true, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + // Add missed lines + for (const line of linesMissed) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // false value means line is NOT covered + const statementCoverage = new StatementCoverage( + false, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + return detailedCoverageArray; + } +} diff --git a/src/client/testing/testController/common/testDiscoveryHandler.ts b/src/client/testing/testController/common/testDiscoveryHandler.ts new file mode 100644 index 000000000000..3f70e6b68594 --- /dev/null +++ b/src/client/testing/testController/common/testDiscoveryHandler.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, Uri, MarkdownString } from 'vscode'; +import * as util from 'util'; +import { DiscoveredTestPayload } from './types'; +import { TestProvider } from '../../types'; +import { traceError, traceWarn } from '../../../logging'; +import { Testing } from '../../../common/utils/localize'; +import { createErrorTestItem } from './testItemUtilities'; +import { buildErrorNodeOptions, populateTestTree } from './utils'; +import { TestItemIndex } from './testItemIndex'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; + +/** + * Stateless handler for processing discovery payloads and building/updating the TestItem tree. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestDiscoveryHandler { + /** + * Process discovery payload and update test tree + * Pure function - no instance state used + */ + public processDiscovery( + payload: DiscoveredTestPayload, + testController: TestController, + testItemIndex: TestItemIndex, + workspaceUri: Uri, + testProvider: TestProvider, + token?: CancellationToken, + projectId?: string, + projectName?: string, + ): void { + if (!payload) { + // No test data is available + return; + } + + const workspacePath = workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; + + // Check if there were any errors in the discovery process. + if (rawTestData.status === 'error') { + this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider, projectId, projectName); + } else { + // remove error node only if no errors exist. + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + testController.items.delete(errorNodeId); + } + + if (rawTestData.tests || rawTestData.tests === null) { + // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. + // parse and insert test data. + + // Clear existing mappings before rebuilding test tree + testItemIndex.clear(); + + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + // Note: populateTestTree will call testItemIndex.registerTestItem() for each discovered test + populateTestTree( + testController, + rawTestData.tests, + undefined, + { + runIdToTestItem: testItemIndex.runIdToTestItemMap, + runIdToVSid: testItemIndex.runIdToVSidMap, + vsIdToRunId: testItemIndex.vsIdToRunIdMap, + }, + token, + projectId, + projectName, + ); + } + } + + /** + * Create an error node for discovery failures + */ + public createErrorNode( + testController: TestController, + workspaceUri: Uri, + error: string[] | undefined, + testProvider: TestProvider, + projectId?: string, + projectName?: string, + ): void { + const workspacePath = workspaceUri.fsPath; + const testingErrorConst = + testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; + + traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? ''); + + // For unittest in project-based mode, check if the error might be caused by nested project imports + // This helps users understand that import errors from nested projects can be safely ignored + // if those tests are covered by a different project with the correct environment. + if (testProvider === 'unittest' && projectId) { + const errorText = error?.join(' ') ?? ''; + const isImportError = + errorText.includes('ModuleNotFoundError') || + errorText.includes('ImportError') || + errorText.includes('No module named'); + + if (isImportError) { + const warningMessage = + '--- ' + + `[test-by-project] Import error during unittest discovery for project at ${workspacePath}. ` + + 'This may be caused by test files in nested project directories that require different dependencies. ' + + 'If these tests are discovered successfully by their own project (with the correct Python environment), ' + + 'this error can be safely ignored. To avoid this, consider excluding nested project paths from parent project discovery. ' + + '---'; + traceWarn(warningMessage); + } + } + + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + let errorNode = testController.items.get(errorNodeId); + const message = util.format( + `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, + error?.join('\r\n\r\n') ?? '', + ); + + if (errorNode === undefined) { + const options = buildErrorNodeOptions(workspaceUri, message, testProvider, projectName); + // Update the error node ID to include project scope if applicable + options.id = errorNodeId; + errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + } + + const errorNodeLabel: MarkdownString = new MarkdownString( + `[Show output](command:python.viewOutput) to view error logs`, + ); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; + } +} diff --git a/src/client/testing/testController/common/testExecutionHandler.ts b/src/client/testing/testController/common/testExecutionHandler.ts new file mode 100644 index 000000000000..127e6980ae46 --- /dev/null +++ b/src/client/testing/testController/common/testExecutionHandler.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestRun, TestMessage, Location } from 'vscode'; +import { ExecutionTestPayload } from './types'; +import { TestItemIndex } from './testItemIndex'; +import { splitLines } from '../../../common/stringUtils'; +import { splitTestNameWithRegex } from './utils'; +import { clearAllChildren } from './testItemUtilities'; + +/** + * Stateless handler for processing execution payloads and updating TestRun instances. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestExecutionHandler { + /** + * Process execution payload and update test run + * Pure function - no instance state used + */ + public processExecution( + payload: ExecutionTestPayload, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTestExecData = payload as ExecutionTestPayload; + + if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { + for (const keyTemp of Object.keys(rawTestExecData.result)) { + const testItem = rawTestExecData.result[keyTemp]; + + // Delegate to specific outcome handlers + this.handleTestOutcome(keyTemp, testItem, runInstance, testItemIndex, testController); + } + } + } + + /** + * Handle a single test result based on outcome + */ + private handleTestOutcome( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + if (testItem.outcome === 'error') { + this.handleTestError(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { + this.handleTestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { + this.handleTestSuccess(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'skipped') { + this.handleTestSkipped(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-failure') { + this.handleSubtestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-success') { + this.handleSubtestSuccess(runId, runInstance, testItemIndex, testController); + } + } + + /** + * Handle test items that errored during execution + */ + private handleTestError( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${testItem.test} failed with error: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.errored(foundItem, message); + } + } + + /** + * Handle test items that failed during execution + */ + private handleTestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.failed(foundItem, message); + } + } + + /** + * Handle test items that passed during execution + */ + private handleTestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.passed(foundItem); + } + } + + /** + * Handle test items that were skipped during execution + */ + private handleTestSkipped( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.skipped(foundItem); + } + } + + /** + * Handle subtest failures + */ + private handleSubtestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.failed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { + failed: 1, + passed: 0, + }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + const traceback = testItem.traceback ?? ''; + const text = `${testItem.subtest} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(text); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + + /** + * Handle subtest successes + */ + private handleSubtestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.passed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { failed: 0, passed: 1 }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } +} diff --git a/src/client/testing/testController/common/testItemIndex.ts b/src/client/testing/testController/common/testItemIndex.ts new file mode 100644 index 000000000000..448903eae7d5 --- /dev/null +++ b/src/client/testing/testController/common/testItemIndex.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem } from 'vscode'; +import { traceError, traceVerbose } from '../../../logging'; +import { getTestCaseNodes } from './testItemUtilities'; + +export interface SubtestStats { + passed: number; + failed: number; +} + +/** + * Maintains persistent ID mappings between Python test IDs and VS Code TestItems. + * This is a stateful component that bridges discovery and execution phases. + * + * Lifecycle: + * - Created: When PythonResultResolver is instantiated (during workspace activation) + * - Populated: During discovery - each discovered test registers its mappings + * - Queried: During execution - to look up TestItems by Python run ID + * - Cleared: When discovery runs again (fresh start) or workspace is disposed + * - Cleaned: Periodically to remove stale references to deleted tests + */ +export class TestItemIndex { + // THE STATE - these maps persist across discovery and execution + private runIdToTestItem: Map; + private runIdToVSid: Map; + private vsIdToRunId: Map; + private subtestStatsMap: Map; + + constructor() { + this.runIdToTestItem = new Map(); + this.runIdToVSid = new Map(); + this.vsIdToRunId = new Map(); + this.subtestStatsMap = new Map(); + } + + /** + * Register a test item with its Python run ID and VS Code ID + * Called during DISCOVERY to populate the index + */ + public registerTestItem(runId: string, vsId: string, testItem: TestItem): void { + this.runIdToTestItem.set(runId, testItem); + this.runIdToVSid.set(runId, vsId); + this.vsIdToRunId.set(vsId, runId); + } + + /** + * Get TestItem by Python run ID (with validation and fallback strategies) + * Called during EXECUTION to look up tests + * + * Uses a three-tier approach: + * 1. Direct O(1) lookup in runIdToTestItem map + * 2. If stale, try vsId mapping and search by VS Code ID + * 3. Last resort: full tree search + */ + public getTestItem(runId: string, testController: TestController): TestItem | undefined { + // Try direct O(1) lookup first + const directItem = this.runIdToTestItem.get(runId); + if (directItem) { + // Validate the item is still in the test tree + if (this.isTestItemValid(directItem, testController)) { + return directItem; + } else { + // Clean up stale reference + this.runIdToTestItem.delete(runId); + } + } + + // Try vsId mapping as fallback + const vsId = this.runIdToVSid.get(runId); + if (vsId) { + // Search by VS Code ID in the controller + let foundItem: TestItem | undefined; + testController.items.forEach((item) => { + if (item.id === vsId) { + foundItem = item; + return; + } + if (!foundItem) { + item.children.forEach((child) => { + if (child.id === vsId) { + foundItem = child; + } + }); + } + }); + + if (foundItem) { + // Cache for future lookups + this.runIdToTestItem.set(runId, foundItem); + return foundItem; + } else { + // Clean up stale mapping + this.runIdToVSid.delete(runId); + this.vsIdToRunId.delete(vsId); + } + } + + // Last resort: full tree search + traceError(`Falling back to tree search for test: ${runId}`); + const testCases = this.collectAllTestCases(testController); + return testCases.find((item) => item.id === vsId); + } + + /** + * Get Python run ID from VS Code ID + * Called by WorkspaceTestAdapter.executeTests() to convert selected tests to Python IDs + */ + public getRunId(vsId: string): string | undefined { + return this.vsIdToRunId.get(vsId); + } + + /** + * Get VS Code ID from Python run ID + */ + public getVSId(runId: string): string | undefined { + return this.runIdToVSid.get(runId); + } + + /** + * Check if a TestItem reference is still valid in the tree + * + * Time Complexity: O(depth) where depth is the maximum nesting level of the test tree. + * In most cases this is O(1) to O(3) since test trees are typically shallow. + */ + public isTestItemValid(testItem: TestItem, testController: TestController): boolean { + // Simple validation: check if the item's parent chain leads back to the controller + let current: TestItem | undefined = testItem; + while (current?.parent) { + current = current.parent; + } + + // If we reached a root item, check if it's in the controller + if (current) { + return testController.items.get(current.id) === current; + } + + // If no parent chain, check if it's directly in the controller + return testController.items.get(testItem.id) === testItem; + } + + /** + * Get subtest statistics for a parent test case + * Returns undefined if no stats exist yet for this parent + */ + public getSubtestStats(parentId: string): SubtestStats | undefined { + return this.subtestStatsMap.get(parentId); + } + + /** + * Set subtest statistics for a parent test case + */ + public setSubtestStats(parentId: string, stats: SubtestStats): void { + this.subtestStatsMap.set(parentId, stats); + } + + /** + * Remove all mappings + * Called at the start of discovery to ensure clean state + */ + public clear(): void { + this.runIdToTestItem.clear(); + this.runIdToVSid.clear(); + this.vsIdToRunId.clear(); + this.subtestStatsMap.clear(); + } + + /** + * Clean up stale references that no longer exist in the test tree + * Called after test tree modifications + */ + public cleanupStaleReferences(testController: TestController): void { + const staleRunIds: string[] = []; + + // Check all runId->TestItem mappings + this.runIdToTestItem.forEach((testItem, runId) => { + if (!this.isTestItemValid(testItem, testController)) { + staleRunIds.push(runId); + } + }); + + // Remove stale entries + staleRunIds.forEach((runId) => { + const vsId = this.runIdToVSid.get(runId); + this.runIdToTestItem.delete(runId); + this.runIdToVSid.delete(runId); + if (vsId) { + this.vsIdToRunId.delete(vsId); + } + }); + + if (staleRunIds.length > 0) { + traceVerbose(`Cleaned up ${staleRunIds.length} stale test item references`); + } + } + + /** + * Collect all test case items from the test controller tree. + * Note: This performs full tree traversal - use cached lookups when possible. + */ + private collectAllTestCases(testController: TestController): TestItem[] { + const testCases: TestItem[] = []; + + testController.items.forEach((i) => { + const tempArr: TestItem[] = getTestCaseNodes(i); + testCases.push(...tempArr); + }); + + return testCases; + } + + // Expose maps for backward compatibility (read-only access) + public get runIdToTestItemMap(): Map { + return this.runIdToTestItem; + } + + public get runIdToVSidMap(): Map { + return this.runIdToVSid; + } + + public get vsIdToRunIdMap(): Map { + return this.vsIdToRunId; + } +} diff --git a/src/client/testing/testController/common/testItemUtilities.ts b/src/client/testing/testController/common/testItemUtilities.ts new file mode 100644 index 000000000000..43624bba2527 --- /dev/null +++ b/src/client/testing/testController/common/testItemUtilities.ts @@ -0,0 +1,583 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { + TestItem, + Uri, + Range, + Position, + TestController, + TestRunResult, + TestResultState, + TestResultSnapshot, + TestItemCollection, +} from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { asyncForEach } from '../../../common/utils/arrayUtils'; +import { traceError, traceVerbose } from '../../../logging'; +import { + RawDiscoveredTests, + RawTest, + RawTestFile, + RawTestFolder, + RawTestFunction, + RawTestSuite, + TestData, + TestDataKinds, +} from './types'; + +// Todo: Use `TestTag` when the proposed API gets into stable. +export const RunTestTag = { id: 'python-run' }; +export const DebugTestTag = { id: 'python-debug' }; + +function testItemCollectionToArray(collection: TestItemCollection): TestItem[] { + const items: TestItem[] = []; + collection.forEach((c) => { + items.push(c); + }); + return items; +} + +export function removeItemByIdFromChildren( + idToRawData: Map, + item: TestItem, + childNodeIdsToRemove: string[], +): void { + childNodeIdsToRemove.forEach((id) => { + item.children.delete(id); + idToRawData.delete(id); + }); +} + +export type ErrorTestItemOptions = { id: string; label: string; error: string }; + +export function createErrorTestItem(testController: TestController, options: ErrorTestItemOptions): TestItem { + const testItem = testController.createTestItem(options.id, options.label); + testItem.canResolveChildren = false; + testItem.error = options.error; + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +export function createWorkspaceRootTestItem( + testController: TestController, + idToRawData: Map, + options: { id: string; label: string; uri: Uri; runId: string; parentId?: string; rawId?: string }, +): TestItem { + const testItem = testController.createTestItem(options.id, options.label, options.uri); + testItem.canResolveChildren = true; + idToRawData.set(options.id, { + ...options, + rawId: options.rawId ?? options.id, + kind: TestDataKinds.Workspace, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function getParentIdFromRawParentId( + idToRawData: Map, + testRoot: string, + raw: { parentid: string }, +): string | undefined { + const parent = idToRawData.get(path.join(testRoot, raw.parentid)); + let parentId; + if (parent) { + parentId = parent.id === '.' ? testRoot : parent.id; + } + return parentId; +} + +function getRangeFromRawSource(raw: { source: string }): Range | undefined { + // We have to extract the line number from the source data. If it is available it + // saves us from running symbol script or querying language server for this info. + try { + const sourceLine = raw.source.substr(raw.source.indexOf(':') + 1); + const line = Number.parseInt(sourceLine, 10); + // Lines in raw data start at 1, vscode lines start at 0 + return new Range(new Position(line - 1, 0), new Position(line, 0)); + } catch (ex) { + // ignore + } + return undefined; +} + +export function getRunIdFromRawData(id: string): string { + // TODO: This is a temporary solution to normalize test ids. + // The current method is error prone and easy to break. When we + // re-write the test adapters we should make sure we consider this. + // This is the id that will be used to compare with the results. + const runId = id + .replace(/\.py[^\w\-]/g, '') // we want to get rid of the `.py` in file names + .replace(/[\\\:\/]/g, '.') + .replace(/\:\:/g, '.') + .replace(/\.\./g, '.'); + return runId.startsWith('.') ? runId.substr(1) : runId; +} + +function createFolderOrFileTestItem( + testController: TestController, + idToRawData: Map, + testRoot: string, + rawData: RawTestFolder | RawTestFile, +): TestItem { + const fullPath = path.join(testRoot, rawData.relpath); + const uri = Uri.file(fullPath); + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + + const label = path.basename(fullPath); + const testItem = testController.createTestItem(fullPath, label, uri); + + testItem.canResolveChildren = true; + + idToRawData.set(testItem.id, { + id: testItem.id, + rawId: rawData.id, + runId: rawData.relpath, + uri, + kind: TestDataKinds.FolderOrFile, + parentId, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function updateFolderOrFileTestItem( + item: TestItem, + idToRawData: Map, + testRoot: string, + rawData: RawTestFolder | RawTestFile, +): void { + const fullPath = path.join(testRoot, rawData.relpath); + const uri = Uri.file(fullPath); + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + + item.label = path.basename(fullPath); + + item.canResolveChildren = true; + + idToRawData.set(item.id, { + id: item.id, + rawId: rawData.id, + runId: rawData.relpath, + uri, + kind: TestDataKinds.FolderOrFile, + parentId, + }); + item.tags = [RunTestTag, DebugTestTag]; +} + +function createCollectionTestItem( + testController: TestController, + idToRawData: Map, + testRoot: string, + rawData: RawTestSuite | RawTestFunction, +): TestItem { + // id can look like test_something.py::SomeClass + const id = path.join(testRoot, rawData.id); + + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.id.substr(0, rawData.id.indexOf(':'))); + const uri = Uri.file(documentPath); + + const label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + const testItem = testController.createTestItem(id, label, uri); + + testItem.canResolveChildren = true; + + idToRawData.set(testItem.id, { + id: testItem.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Collection, + parentId, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function updateCollectionTestItem( + item: TestItem, + idToRawData: Map, + testRoot: string, + rawData: RawTestSuite | RawTestFunction, +): void { + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.id.substr(0, rawData.id.indexOf(':'))); + const uri = Uri.file(documentPath); + + item.label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + item.canResolveChildren = true; + + idToRawData.set(item.id, { + id: item.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Collection, + parentId, + }); + item.tags = [RunTestTag, DebugTestTag]; +} + +function createTestCaseItem( + testController: TestController, + idToRawData: Map, + testRoot: string, + rawData: RawTest, +): TestItem { + // id can look like: + // test_something.py::SomeClass::someTest + // test_something.py::SomeClass::someTest[x1] + const id = path.join(testRoot, rawData.id); + + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.source.substr(0, rawData.source.indexOf(':'))); + const uri = Uri.file(documentPath); + + const label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + const testItem = testController.createTestItem(id, label, uri); + + testItem.canResolveChildren = false; + testItem.range = getRangeFromRawSource(rawData); + + idToRawData.set(testItem.id, { + id: testItem.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Case, + parentId, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function updateTestCaseItem( + item: TestItem, + idToRawData: Map, + testRoot: string, + rawData: RawTest, +): void { + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.source.substr(0, rawData.source.indexOf(':'))); + const uri = Uri.file(documentPath); + + item.label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + item.canResolveChildren = false; + item.range = getRangeFromRawSource(rawData); + + idToRawData.set(item.id, { + id: item.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Case, + parentId, + }); + item.tags = [RunTestTag, DebugTestTag]; +} + +async function updateTestItemFromRawDataInternal( + item: TestItem, + testController: TestController, + idToRawData: Map, + testRoot: string, + rawDataSet: RawDiscoveredTests[], + token?: CancellationToken, +): Promise { + if (token?.isCancellationRequested) { + return; + } + + const rawId = idToRawData.get(item.id)?.rawId; + if (!rawId) { + traceError(`Unknown node id: ${item.id}`); + return; + } + + const nodeRawData = rawDataSet.filter( + (r) => + r.root === rawId || + r.rootid === rawId || + r.parents.find((p) => p.id === rawId) || + r.tests.find((t) => t.id === rawId), + ); + + if (nodeRawData.length === 0 && item.parent) { + removeItemByIdFromChildren(idToRawData, item.parent, [item.id]); + traceVerbose(`Following test item was removed Reason: No-Raw-Data ${item.id}`); + return; + } + + if (nodeRawData.length > 1) { + // Something is wrong, there can only be one test node with that id + traceError(`Multiple (${nodeRawData.length}) raw data nodes had the same id: ${rawId}`); + return; + } + + if (rawId === nodeRawData[0].root || rawId === nodeRawData[0].rootid) { + // This is a test root node, we need to update the entire tree + // The update children and remove any child that does not have raw data. + + await asyncForEach(testItemCollectionToArray(item.children), async (c) => { + await updateTestItemFromRawData(c, testController, idToRawData, testRoot, nodeRawData, token); + }); + + // Create child nodes that are new. + // We only need to look at rawData.parents. Since at this level we either have folder or file. + const rawChildNodes = nodeRawData[0].parents.filter((p) => p.parentid === '.' || p.parentid === rawId); + const existingNodes: string[] = []; + item.children.forEach((c) => existingNodes.push(idToRawData.get(c.id)?.rawId ?? '')); + + await asyncForEach( + rawChildNodes.filter((r) => !existingNodes.includes(r.id)), + async (r) => { + const childItem = + r.kind === 'file' + ? createFolderOrFileTestItem(testController, idToRawData, testRoot, r as RawTestFile) + : createFolderOrFileTestItem(testController, idToRawData, testRoot, r as RawTestFolder); + item.children.add(childItem); + await updateTestItemFromRawData(childItem, testController, idToRawData, testRoot, nodeRawData, token); + }, + ); + + return; + } + + // First check if this is a parent node + const rawData = nodeRawData[0].parents.filter((r) => r.id === rawId); + if (rawData.length === 1) { + // This is either a File/Folder/Collection node + + // Update the node data + switch (rawData[0].kind) { + case 'file': + updateFolderOrFileTestItem(item, idToRawData, testRoot, rawData[0] as RawTestFile); + break; + case 'folder': + updateFolderOrFileTestItem(item, idToRawData, testRoot, rawData[0] as RawTestFolder); + break; + case 'suite': + updateCollectionTestItem(item, idToRawData, testRoot, rawData[0] as RawTestSuite); + break; + case 'function': + updateCollectionTestItem(item, idToRawData, testRoot, rawData[0] as RawTestFunction); + break; + default: + break; + } + + // The update children and remove any child that does not have raw data. + await asyncForEach(testItemCollectionToArray(item.children), async (c) => { + await updateTestItemFromRawData(c, testController, idToRawData, testRoot, nodeRawData, token); + }); + + // Create child nodes that are new. + // Get the existing child node ids so we can skip them + const existingNodes: string[] = []; + item.children.forEach((c) => existingNodes.push(idToRawData.get(c.id)?.rawId ?? '')); + + // We first look at rawData.parents. Since at this level we either have folder or file. + // The current node is potentially a parent of one of these "parent" nodes or it is a parent + // of test case nodes. We will handle Test case nodes after handling parents. + const rawChildNodes = nodeRawData[0].parents.filter((p) => p.parentid === rawId); + await asyncForEach( + rawChildNodes.filter((r) => !existingNodes.includes(r.id)), + async (r) => { + let childItem; + switch (r.kind) { + case 'file': + childItem = createFolderOrFileTestItem(testController, idToRawData, testRoot, r as RawTestFile); + break; + case 'folder': + childItem = createFolderOrFileTestItem( + testController, + idToRawData, + testRoot, + r as RawTestFolder, + ); + break; + case 'suite': + childItem = createCollectionTestItem(testController, idToRawData, testRoot, r as RawTestSuite); + break; + case 'function': + childItem = createCollectionTestItem( + testController, + idToRawData, + testRoot, + r as RawTestFunction, + ); + break; + default: + break; + } + if (childItem) { + item.children.add(childItem); + // This node can potentially have children. So treat it like a new node and update it. + await updateTestItemFromRawData( + childItem, + testController, + idToRawData, + testRoot, + nodeRawData, + token, + ); + } + }, + ); + + // Now we will look at test case nodes. Create any test case node that does not already exist. + const rawTestCaseNodes = nodeRawData[0].tests.filter((p) => p.parentid === rawId); + rawTestCaseNodes + .filter((r) => !existingNodes.includes(r.id)) + .forEach((r) => { + const childItem = createTestCaseItem(testController, idToRawData, testRoot, r); + item.children.add(childItem); + }); + + return; + } + + if (rawData.length > 1) { + // Something is wrong, there can only be one test node with that id + traceError(`Multiple (${rawData.length}) raw data nodes had the same id: ${rawId}`); + return; + } + + // We are here this means rawData.length === 0 + // The node is probably is test case node. Try and find it. + const rawCaseData = nodeRawData[0].tests.filter((r) => r.id === rawId); + + if (rawCaseData.length === 1) { + // This is a test case node + updateTestCaseItem(item, idToRawData, testRoot, rawCaseData[0]); + return; + } + + if (rawCaseData.length > 1) { + // Something is wrong, there can only be one test node with that id + traceError(`Multiple (${rawCaseData.length}) raw data nodes had the same id: ${rawId}`); + } +} + +export async function updateTestItemFromRawData( + item: TestItem, + testController: TestController, + idToRawData: Map, + testRoot: string, + rawDataSet: RawDiscoveredTests[], + token?: CancellationToken, +): Promise { + item.busy = true; + await updateTestItemFromRawDataInternal(item, testController, idToRawData, testRoot, rawDataSet, token); + item.busy = false; +} + +export function getTestCaseNodes(testNode: TestItem, collection: TestItem[] = []): TestItem[] { + if (!testNode.canResolveChildren && testNode.tags.length > 0) { + collection.push(testNode); + } + + testNode.children.forEach((c) => { + if (testNode.canResolveChildren) { + getTestCaseNodes(c, collection); + } else { + collection.push(testNode); + } + }); + return collection; +} + +export function getWorkspaceNode(testNode: TestItem, idToRawData: Map): TestItem | undefined { + const raw = idToRawData.get(testNode.id); + if (raw) { + if (raw.kind === TestDataKinds.Workspace) { + return testNode; + } + if (testNode.parent) { + return getWorkspaceNode(testNode.parent, idToRawData); + } + } + return undefined; +} + +export function getNodeByUri(root: TestItem, uri: Uri): TestItem | undefined { + if (root.uri?.fsPath === uri.fsPath) { + return root; + } + + const nodes: TestItem[] = []; + root.children.forEach((c) => nodes.push(c)); + + // Search at the current level + for (const node of nodes) { + if (node.uri?.fsPath === uri.fsPath) { + return node; + } + } + + // Search the children of the current level + for (const node of nodes) { + const found = getNodeByUri(node, uri); + if (found) { + return found; + } + } + return undefined; +} + +function updateTestResultMapForSnapshot(resultMap: Map, snapshot: TestResultSnapshot) { + for (const taskState of snapshot.taskStates) { + resultMap.set(snapshot.id, taskState.state); + } + snapshot.children.forEach((child) => updateTestResultMapForSnapshot(resultMap, child)); +} + +export function updateTestResultMap( + resultMap: Map, + testResults: readonly TestRunResult[], +): void { + const ordered = new Array(...testResults).sort((a, b) => a.completedAt - b.completedAt); + ordered.forEach((testResult) => { + testResult.results.forEach((snapshot) => updateTestResultMapForSnapshot(resultMap, snapshot)); + }); +} + +export function checkForFailedTests(resultMap: Map): boolean { + return ( + Array.from(resultMap.values()).find( + (state) => state === TestResultState.Failed || state === TestResultState.Errored, + ) !== undefined + ); +} + +export function clearAllChildren(testNode: TestItem): void { + const ids: string[] = []; + testNode.children.forEach((c) => ids.push(c.id)); + ids.forEach(testNode.children.delete); +} diff --git a/src/client/testing/testController/common/testProjectRegistry.ts b/src/client/testing/testController/common/testProjectRegistry.ts new file mode 100644 index 000000000000..4f0702ad584c --- /dev/null +++ b/src/client/testing/testController/common/testProjectRegistry.ts @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { TestController, Uri } from 'vscode'; +import { isParentPath } from '../../../common/platform/fs-paths'; +import { IConfigurationService } from '../../../common/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { traceError, traceInfo } from '../../../logging'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonProject, PythonEnvironment } from '../../../envExt/types'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from './projectAdapter'; +import { getProjectId, createProjectDisplayName, createTestAdapters } from './projectUtils'; +import { PythonResultResolver } from './resultResolver'; + +/** + * Registry for Python test projects within workspaces. + * + * Manages the lifecycle of test projects including: + * - Discovering Python projects via Python Environments API + * - Creating and storing ProjectAdapter instances per workspace + * - Computing nested project relationships for ignore lists + * - Fallback to default "legacy" project when API unavailable + * + * **Key concepts:** + * - **Workspace:** A VS Code workspace folder (may contain multiple projects) + * - **Project:** A Python project within a workspace (identified by pyproject.toml, setup.py, etc.) + * - **ProjectUri:** The unique identifier for a project (the URI of the project root directory) + * - Each project gets its own test tree root, Python environment, and test adapters + * + * **Project identification:** + * Projects are identified and tracked by their URI (projectUri.toString()). This matches + * how the Python Environments extension stores projects in its Map. + */ +export class TestProjectRegistry { + /** + * Map of workspace URI -> Map of project URI string -> ProjectAdapter + * + * Projects are keyed by their URI string (projectUri.toString()) which matches how + * the Python Environments extension identifies projects. This enables O(1) lookups + * when given a project URI. + */ + private readonly workspaceProjects: Map> = new Map(); + + constructor( + private readonly testController: TestController, + private readonly configSettings: IConfigurationService, + private readonly interpreterService: IInterpreterService, + private readonly envVarsService: IEnvironmentVariablesProvider, + ) {} + + /** + * Gets the projects map for a workspace, if it exists. + */ + public getWorkspaceProjects(workspaceUri: Uri): Map | undefined { + return this.workspaceProjects.get(workspaceUri); + } + + /** + * Checks if a workspace has been initialized with projects. + */ + public hasProjects(workspaceUri: Uri): boolean { + return this.workspaceProjects.has(workspaceUri); + } + + /** + * Gets all projects for a workspace as an array. + */ + public getProjectsArray(workspaceUri: Uri): ProjectAdapter[] { + const projectsMap = this.workspaceProjects.get(workspaceUri); + return projectsMap ? Array.from(projectsMap.values()) : []; + } + + /** + * Discovers and registers all Python projects for a workspace. + * Returns the discovered projects for the caller to use. + */ + public async discoverAndRegisterProjects(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Discovering projects for workspace: ${workspaceUri.fsPath}`); + + const projects = await this.discoverProjects(workspaceUri); + + // Create map for this workspace, keyed by project URI + const projectsMap = new Map(); + projects.forEach((project) => { + projectsMap.set(getProjectId(project.projectUri), project); + }); + + this.workspaceProjects.set(workspaceUri, projectsMap); + traceInfo(`[test-by-project] Registered ${projects.length} project(s) for ${workspaceUri.fsPath}`); + + return projects; + } + + /** + * Computes and populates nested project ignore lists for all projects in a workspace. + * Must be called before discovery to ensure parent projects ignore nested children. + */ + public configureNestedProjectIgnores(workspaceUri: Uri): void { + const projectIgnores = this.computeNestedProjectIgnores(workspaceUri); + const projects = this.getProjectsArray(workspaceUri); + + for (const project of projects) { + const ignorePaths = projectIgnores.get(getProjectId(project.projectUri)); + if (ignorePaths && ignorePaths.length > 0) { + project.nestedProjectPathsToIgnore = ignorePaths; + traceInfo(`[test-by-project] ${project.projectName} will ignore nested: ${ignorePaths.join(', ')}`); + } + } + } + + /** + * Clears all projects for a workspace. + */ + public clearWorkspace(workspaceUri: Uri): void { + this.workspaceProjects.delete(workspaceUri); + } + + // ====== Private Methods ====== + + /** + * Discovers Python projects in a workspace using the Python Environment API. + * Falls back to creating a single default project if API is unavailable. + */ + private async discoverProjects(workspaceUri: Uri): Promise { + try { + if (!useEnvExtension()) { + traceInfo('[test-by-project] Python Environments API not available, using default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + const envExtApi = await getEnvExtApi(); + const allProjects = envExtApi.getPythonProjects(); + traceInfo(`[test-by-project] Found ${allProjects.length} total Python projects from API`); + + // Filter to projects within this workspace + const workspaceProjects = allProjects.filter((project) => + isParentPath(project.uri.fsPath, workspaceUri.fsPath), + ); + traceInfo(`[test-by-project] Filtered to ${workspaceProjects.length} projects in workspace`); + + if (workspaceProjects.length === 0) { + traceInfo('[test-by-project] No projects found, creating default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + // Create ProjectAdapter for each discovered project + const adapters: ProjectAdapter[] = []; + for (const pythonProject of workspaceProjects) { + try { + const adapter = await this.createProjectAdapter(pythonProject, workspaceUri); + adapters.push(adapter); + } catch (error) { + traceError(`[test-by-project] Failed to create adapter for ${pythonProject.uri.fsPath}:`, error); + } + } + + if (adapters.length === 0) { + traceInfo('[test-by-project] All adapters failed, falling back to default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + return adapters; + } catch (error) { + traceError('[test-by-project] Discovery failed, using default project:', error); + return [await this.createDefaultProject(workspaceUri)]; + } + } + + /** + * Creates a ProjectAdapter from a PythonProject. + * + * Each project gets its own isolated test infrastructure: + * - **ResultResolver:** Handles mapping test IDs and processing results for this project + * - **DiscoveryAdapter:** Discovers tests scoped to this project's root directory + * - **ExecutionAdapter:** Runs tests for this project using its Python environment + * + */ + private async createProjectAdapter(pythonProject: PythonProject, workspaceUri: Uri): Promise { + const projectId = getProjectId(pythonProject.uri); + traceInfo(`[test-by-project] Creating adapter for: ${pythonProject.name} at ${projectId}`); + + // Resolve Python environment + const envExtApi = await getEnvExtApi(); + const pythonEnvironment = await envExtApi.getEnvironment(pythonProject.uri); + if (!pythonEnvironment) { + throw new Error(`No Python environment found for project ${projectId}`); + } + + // Create test infrastructure + const testProvider = this.getTestProvider(workspaceUri); + const projectDisplayName = createProjectDisplayName(pythonProject.name, pythonEnvironment.version); + const resultResolver = new PythonResultResolver( + this.testController, + testProvider, + workspaceUri, + projectId, + pythonProject.name, // Use simple project name for test tree label (without version) + ); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + return { + projectName: projectDisplayName, + projectUri: pythonProject.uri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Creates a default project for legacy/fallback mode. + */ + private async createDefaultProject(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Creating default project for: ${workspaceUri.fsPath}`); + + const testProvider = this.getTestProvider(workspaceUri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspaceUri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const interpreter = await this.interpreterService.getActiveInterpreter(workspaceUri); + + const pythonEnvironment: PythonEnvironment = { + name: 'default', + displayName: interpreter?.displayName || 'Python', + shortDisplayName: interpreter?.displayName || 'Python', + displayPath: interpreter?.path || 'python', + version: interpreter?.version?.raw || '3.x', + environmentPath: Uri.file(interpreter?.path || 'python'), + sysPrefix: interpreter?.sysPrefix || '', + execInfo: { run: { executable: interpreter?.path || 'python' } }, + envId: { id: 'default', managerId: 'default' }, + }; + + const pythonProject: PythonProject = { + name: path.basename(workspaceUri.fsPath) || 'workspace', + uri: workspaceUri, + }; + + return { + projectName: pythonProject.name, + projectUri: workspaceUri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Identifies nested projects and returns ignore paths for parent projects. + * + * **Time complexity:** O(n²) where n is the number of projects in the workspace. + * For each project, checks all other projects to find nested relationships. + * + * Note: Uses path.normalize() to handle Windows path separator inconsistencies + * (e.g., paths from URI.fsPath may have mixed separators). + */ + private computeNestedProjectIgnores(workspaceUri: Uri): Map { + const ignoreMap = new Map(); + const projects = this.getProjectsArray(workspaceUri); + + if (projects.length === 0) return ignoreMap; + + for (const parent of projects) { + const nestedPaths: string[] = []; + + for (const child of projects) { + // Skip self-comparison using URI + if (parent.projectUri.toString() === child.projectUri.toString()) continue; + + // Normalize paths to handle Windows path separator inconsistencies + const parentNormalized = path.normalize(parent.projectUri.fsPath); + const childNormalized = path.normalize(child.projectUri.fsPath); + + // Add trailing separator to ensure we match directory boundaries + const parentWithSep = parentNormalized.endsWith(path.sep) + ? parentNormalized + : parentNormalized + path.sep; + const childWithSep = childNormalized.endsWith(path.sep) ? childNormalized : childNormalized + path.sep; + + // Check if child is inside parent (case-insensitive for Windows) + const childIsInsideParent = childWithSep.toLowerCase().startsWith(parentWithSep.toLowerCase()); + + if (childIsInsideParent) { + nestedPaths.push(child.projectUri.fsPath); + traceInfo(`[test-by-project] Nested: ${child.projectName} is inside ${parent.projectName}`); + } + } + + if (nestedPaths.length > 0) { + ignoreMap.set(getProjectId(parent.projectUri), nestedPaths); + } + } + + return ignoreMap; + } + + /** + * Determines the test provider based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : 'pytest'; + } +} diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts new file mode 100644 index 000000000000..017c41cf3d97 --- /dev/null +++ b/src/client/testing/testController/common/types.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + Event, + FileCoverageDetail, + OutputChannel, + TestController, + TestItem, + TestRun, + TestRunProfileKind, + Uri, + WorkspaceFolder, +} from 'vscode'; +import { ITestDebugLauncher } from '../../common/types'; +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from './projectAdapter'; + +export enum TestDataKinds { + Workspace, + FolderOrFile, + Collection, + Case, +} + +export interface TestData { + rawId: string; + runId: string; + id: string; + uri: Uri; + parentId?: string; + kind: TestDataKinds; +} + +export type TestRefreshOptions = { forceRefresh: boolean }; + +export const ITestController = Symbol('ITestController'); +export interface ITestController { + refreshTestData(resource?: Uri, options?: TestRefreshOptions): Promise; + stopRefreshing(): void; + onRefreshingCompleted: Event; + onRefreshingStarted: Event; + onRunWithoutConfiguration: Event; +} + +export const ITestFrameworkController = Symbol('ITestFrameworkController'); +export interface ITestFrameworkController { + resolveChildren(testController: TestController, item: TestItem, token?: CancellationToken): Promise; +} + +export const ITestsRunner = Symbol('ITestsRunner'); +export interface ITestsRunner {} + +// We expose these here as a convenience and to cut down on churn +// elsewhere in the code. +type RawTestNode = { + id: string; + name: string; + parentid: string; +}; +export type RawTestParent = RawTestNode & { + kind: 'folder' | 'file' | 'suite' | 'function' | 'workspace'; +}; +type RawTestFSNode = RawTestParent & { + kind: 'folder' | 'file'; + relpath: string; +}; +export type RawTestFolder = RawTestFSNode & { + kind: 'folder'; +}; +export type RawTestFile = RawTestFSNode & { + kind: 'file'; +}; +export type RawTestSuite = RawTestParent & { + kind: 'suite'; +}; +// function-as-a-container is for parameterized ("sub") tests. +export type RawTestFunction = RawTestParent & { + kind: 'function'; +}; +export type RawTest = RawTestNode & { + source: string; +}; +export type RawDiscoveredTests = { + rootid: string; + root: string; + parents: RawTestParent[]; + tests: RawTest[]; +}; + +// New test discovery adapter types + +export type DataReceivedEvent = { + uuid: string; + data: string; +}; + +export type TestDiscoveryCommand = { + script: string; + args: string[]; +}; + +export type TestExecutionCommand = { + script: string; + args: string[]; +}; + +export type TestCommandOptions = { + workspaceFolder: Uri; + cwd: string; + command: TestDiscoveryCommand | TestExecutionCommand; + token?: CancellationToken; + outChannel?: OutputChannel; + profileKind?: TestRunProfileKind; + testIds?: string[]; +}; + +// /** +// * Interface describing the server that will send test commands to the Python side, and process responses. +// * +// * Consumers will call sendCommand in order to execute Python-related code, +// * and will subscribe to the onDataReceived event to wait for the results. +// */ +// export interface ITestServer { +// readonly onDataReceived: Event; +// readonly onRunDataReceived: Event; +// readonly onDiscoveryDataReceived: Event; +// sendCommand( +// options: TestCommandOptions, +// env: EnvironmentVariables, +// runTestIdsPort?: string, +// runInstance?: TestRun, +// testIds?: string[], +// callback?: () => void, +// executionFactory?: IPythonExecutionFactory, +// ): Promise; +// serverReady(): Promise; +// getPort(): number; +// createUUID(cwd: string): string; +// deleteUUID(uuid: string): void; +// triggerRunDataReceivedEvent(data: DataReceivedEvent): void; +// triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; +// } + +/** + * Test item mapping interface used by populateTestTree. + * Contains only the maps needed for building the test tree. + */ +export interface ITestItemMappings { + runIdToVSid: Map; + runIdToTestItem: Map; + vsIdToRunId: Map; +} + +export interface ITestResultResolver extends ITestItemMappings { + detailedCoverageMap: Map; + + resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void; + resolveExecution(payload: ExecutionTestPayload | CoveragePayload, runInstance: TestRun): void; + _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void; + _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void; + _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void; +} +export interface ITestDiscoveryAdapter { + discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise; +} + +// interface for execution/runner adapter +export interface ITestExecutionAdapter { + runTests( + uri: Uri, + testIds: string[], + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise; +} + +// Same types as in python_files/unittestadapter/utils.py +export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'function' | 'test'; + +export type DiscoveredTestCommon = { + path: string; + name: string; + // Trailing underscore to avoid collision with the 'type' Python keyword. + type_: DiscoveredTestType; + id_: string; +}; + +export type DiscoveredTestItem = DiscoveredTestCommon & { + lineno: number | string; + runID: string; +}; + +export type DiscoveredTestNode = DiscoveredTestCommon & { + children: (DiscoveredTestNode | DiscoveredTestItem)[]; + lineno?: number | string; +}; + +export type DiscoveredTestPayload = { + cwd: string; + tests?: DiscoveredTestNode; + status: 'success' | 'error'; + error?: string[]; +}; + +export type CoveragePayload = { + coverage: boolean; + cwd: string; + result?: { + [filePathStr: string]: FileCoverageMetrics; + }; + error: string; +}; + +// using camel-case for these types to match the python side +export type FileCoverageMetrics = { + // eslint-disable-next-line camelcase + lines_covered: number[]; + // eslint-disable-next-line camelcase + lines_missed: number[]; + executed_branches: number; + total_branches: number; +}; + +export type ExecutionTestPayload = { + cwd: string; + status: 'success' | 'error'; + result?: { + [testRunID: string]: { + test?: string; + outcome?: string; + message?: string; + traceback?: string; + subtest?: string; + }; + }; + notFound?: string[]; + error: string; +}; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts new file mode 100644 index 000000000000..9782487d940b --- /dev/null +++ b/src/client/testing/testController/common/utils.ts @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import { CancellationToken, Position, TestController, TestItem, Uri, Range, Disposable } from 'vscode'; +import { Message } from 'vscode-jsonrpc'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; +import { + DiscoveredTestItem, + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, + ITestItemMappings, +} from './types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; +import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; + +export function fixLogLinesNoTrailing(content: string): string { + const lines = content.split(/\r?\n/g); + return `${lines.join('\r\n')}`; +} +export function createTestingDeferred(): Deferred { + return createDeferred(); +} + +interface ExecutionResultMessage extends Message { + params: ExecutionTestPayload; +} + +/** + * Retrieves the path to the temporary directory. + * + * On Windows, it returns the default temporary directory. + * On macOS/Linux, it prefers the `XDG_RUNTIME_DIR` environment variable if set, + * otherwise, it falls back to the default temporary directory. + * + * @returns {string} The path to the temporary directory. + */ +function getTempDir(): string { + if (process.platform === 'win32') { + return os.tmpdir(); // Default Windows behavior + } + return process.env.XDG_RUNTIME_DIR || os.tmpdir(); // Prefer XDG_RUNTIME_DIR on macOS/Linux +} + +/** + * Writes an array of test IDs to a temporary file. + * + * @param testIds - The array of test IDs to write. + * @returns A promise that resolves to the file name of the temporary file. + */ +export async function writeTestIdsFile(testIds: string[]): Promise { + // temp file name in format of test-ids-.txt + const randomSuffix = crypto.randomBytes(10).toString('hex'); + const tempName = `test-ids-${randomSuffix}.txt`; + // create temp file + let tempFileName: string; + const tempDir: string = getTempDir(); + try { + traceLog('Attempting to use temp directory for test ids file, file name:', tempName); + tempFileName = path.join(tempDir, tempName); + // attempt access to written file to check permissions + await fs.promises.access(tempDir); + } catch (error) { + // Handle the error when accessing the temp directory + traceError('Error accessing temp directory:', error, ' Attempt to use extension root dir instead'); + // Make new temp directory in extension root dir + const tempDir = path.join(EXTENSION_ROOT_DIR, '.temp'); + await fs.promises.mkdir(tempDir, { recursive: true }); + tempFileName = path.join(EXTENSION_ROOT_DIR, '.temp', tempName); + traceLog('New temp file:', tempFileName); + } + // write test ids to file + await fs.promises.writeFile(tempFileName, testIds.join('\n')); + // return file name + return tempFileName; +} + +export async function startRunResultNamedPipe( + dataReceivedCallback: (payload: ExecutionTestPayload) => void, + deferredTillServerClose: Deferred, + cancellationToken?: CancellationToken, +): Promise { + traceVerbose('Starting Test Result named pipe'); + const pipeName: string = generateRandomPipeName('python-test-results'); + + const reader = await createReaderPipe(pipeName, cancellationToken); + traceVerbose(`Test Results named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Results named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; + deferredTillServerClose.resolve(); + }); + + if (cancellationToken) { + disposables.push( + cancellationToken?.onCancellationRequested(() => { + traceLog(`Test Result named pipe ${pipeName} cancelled`); + disposable.dispose(); + }), + ); + } + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Result named pipe ${pipeName} received data`); + // if EOT, call decrement connection count (callback) + dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); + }), + reader.onClose(() => { + // this is called once the server close, once per run instance + traceVerbose(`Test Result named pipe ${pipeName} closed. Disposing of listener/s.`); + // dispose of all data listeners and cancelation listeners + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Results named pipe ${pipeName} error:`, error); + }), + ); + + return pipeName; +} + +interface DiscoveryResultMessage extends Message { + params: DiscoveredTestPayload; +} + +export async function startDiscoveryNamedPipe( + callback: (payload: DiscoveredTestPayload) => void, + cancellationToken?: CancellationToken, +): Promise { + traceVerbose('Starting Test Discovery named pipe'); + // const pipeName: string = '/Users/eleanorboyd/testingFiles/inc_dec_example/temp33.txt'; + const pipeName: string = generateRandomPipeName('python-test-discovery'); + const reader = await createReaderPipe(pipeName, cancellationToken); + + traceVerbose(`Test Discovery named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; + }); + + if (cancellationToken) { + disposables.push( + cancellationToken.onCancellationRequested(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} cancelled`); + disposable.dispose(); + }), + ); + } + + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Discovery named pipe ${pipeName} received data`); + callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); + }), + reader.onClose(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} closed`); + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Discovery named pipe ${pipeName} error:`, error); + }), + ); + return pipeName; +} + +/** + * Extracts the missing module name from a ModuleNotFoundError or ImportError message. + * @param message The error message to parse + * @returns The module name if found, undefined otherwise + */ +function extractMissingModuleName(message: string): string | undefined { + // Match patterns like: + // - No module named 'requests' + // - No module named "requests" + // - ModuleNotFoundError: No module named 'requests' + // - ImportError: No module named requests + const patterns = [/No module named ['"]([^'"]+)['"]/, /No module named (\S+)/]; + + for (const pattern of patterns) { + const match = message.match(pattern); + if (match) { + return match[1]; + } + } + return undefined; +} + +export function buildErrorNodeOptions( + uri: Uri, + message: string, + testType: string, + projectName?: string, +): ErrorTestItemOptions { + let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; + let errorMessage = message; + + // Check for missing module errors and provide specific messaging + const missingModule = extractMissingModuleName(message); + if (missingModule) { + labelText = `Missing Module: ${missingModule}`; + errorMessage = `The module '${missingModule}' is not installed in the selected Python environment. Please install it to enable test discovery.`; + } + + // Use project name for label if available (project-based testing), otherwise use folder name + const displayName = projectName ?? path.basename(uri.fsPath); + + return { + id: `DiscoveryError:${uri.fsPath}`, + label: `${labelText} [${displayName}]`, + error: errorMessage, + }; +} + +export function populateTestTree( + testController: TestController, + testTreeData: DiscoveredTestNode, + testRoot: TestItem | undefined, + testItemMappings: ITestItemMappings, + token?: CancellationToken, + projectId?: string, + projectName?: string, +): void { + // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. + if (!testRoot) { + // Create project-scoped ID if projectId is provided + const rootId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${testTreeData.path}` : testTreeData.path; + // Use "Project: {name}" label for project-based testing, otherwise use folder name + const rootLabel = projectName ? `Project: ${projectName}` : testTreeData.name; + testRoot = testController.createTestItem(rootId, rootLabel, Uri.file(testTreeData.path)); + + testRoot.canResolveChildren = true; + testRoot.tags = [RunTestTag, DebugTestTag]; + + testController.items.add(testRoot); + } + + // Recursively populate the tree with test data. + testTreeData.children.forEach((child) => { + if (!token?.isCancellationRequested) { + if (isTestItem(child)) { + // Create project-scoped vsId + const vsId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + const testItem = testController.createTestItem(vsId, child.name, Uri.file(child.path)); + testItem.tags = [RunTestTag, DebugTestTag]; + + let range: Range | undefined; + if (child.lineno) { + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } + } + testItem.canResolveChildren = false; + testItem.range = range; + testItem.tags = [RunTestTag, DebugTestTag]; + + testRoot!.children.add(testItem); + // add to our map - use runID as key, vsId as value + testItemMappings.runIdToTestItem.set(child.runID, testItem); + testItemMappings.runIdToVSid.set(child.runID, vsId); + testItemMappings.vsIdToRunId.set(vsId, child.runID); + } else { + // Use project-scoped ID for non-test nodes and look up within the current root + const nodeId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + let node = testRoot!.children.get(nodeId); + + if (!node) { + node = testController.createTestItem(nodeId, child.name, Uri.file(child.path)); + + node.canResolveChildren = true; + node.tags = [RunTestTag, DebugTestTag]; + + // Set range for class nodes (and other nodes) if lineno is available + let range: Range | undefined; + if ('lineno' in child && child.lineno) { + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } + node.range = range; + } + + testRoot!.children.add(node); + } + populateTestTree(testController, child, node, testItemMappings, token, projectId, projectName); + } + } + }); +} + +function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { + return test.type_ === 'test'; +} + +export function createExecutionErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + testIds: string[], + cwd: string, +): ExecutionTestPayload { + const etp: ExecutionTestPayload = { + cwd, + status: 'error', + error: `Test run failed, the python test process was terminated before it could exit on its own for workspace ${cwd}`, + result: {}, + }; + // add error result for each attempted test. + for (let i = 0; i < testIds.length; i = i + 1) { + const test = testIds[i]; + etp.result![test] = { + test, + outcome: 'error', + message: ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + }; + } + return etp; +} + +export function createDiscoveryErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + cwd: string, +): DiscoveredTestPayload { + return { + cwd, + status: 'error', + error: [ + ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal} for workspace ${cwd}`, + ], + }; +} + +/** + * Splits a test name into its parent test name and subtest unique section. + * + * @param testName The full test name string. + * @returns A tuple where the first item is the parent test name and the second item is the subtest section or `testName` if no subtest section exists. + */ +export function splitTestNameWithRegex(testName: string): [string, string] { + // If a match is found, return the parent test name and the subtest (whichever was captured between parenthesis or square brackets). + // Otherwise, return the entire testName for the parent and entire testName for the subtest. + const regex = /^(.*?) ([\[(].*[\])])$/; + const match = testName.match(regex); + if (match) { + return [match[1].trim(), match[2] || match[3] || testName]; + } + return [testName, testName]; +} + +/** + * Takes a list of arguments and adds an key-value pair to the list if the key doesn't already exist. Searches each element + * in the array for the key to see if it is contained within the element. + * @param args list of arguments to search + * @param argToAdd argument to add if it doesn't already exist + * @returns the list of arguments with the key-value pair added if it didn't already exist + */ +export function addValueIfKeyNotExist(args: string[], key: string, value: string | null): string[] { + for (const arg of args) { + if (arg.includes(key)) { + traceInfo(`arg: ${key} already exists in args, not adding.`); + return args; + } + } + if (value) { + args.push(`${key}=${value}`); + } else { + args.push(`${key}`); + } + return args; +} + +/** + * Checks if a key exists in a list of arguments. Searches each element in the array + * for the key to see if it is contained within the element. + * @param args list of arguments to search + * @param key string to search for + * @returns true if the key exists in the list of arguments, false otherwise + */ +export function argKeyExists(args: string[], key: string): boolean { + for (const arg of args) { + if (arg.includes(key)) { + return true; + } + } + return false; +} + +/** + * Checks recursively if any parent directories of the given path are symbolic links. + * @param {string} currentPath - The path to start checking from. + * @returns {Promise} - Returns true if any parent directory is a symlink, otherwise false. + */ +export async function hasSymlinkParent(currentPath: string): Promise { + try { + // Resolve the path to an absolute path + const absolutePath = path.resolve(currentPath); + // Get the parent directory + const parentDirectory = path.dirname(absolutePath); + // Check if the current directory is the root directory + if (parentDirectory === absolutePath) { + return false; + } + // Check if the parent directory is a symlink + const stats = await fs.promises.lstat(parentDirectory); + if (stats.isSymbolicLink()) { + traceLog(`Symlink found at: ${parentDirectory}`); + return true; + } + // Recurse up the directory tree + return await hasSymlinkParent(parentDirectory); + } catch (error) { + traceError('Error checking symlinks:', error); + return false; + } +} diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts new file mode 100644 index 000000000000..04de209c171d --- /dev/null +++ b/src/client/testing/testController/controller.ts @@ -0,0 +1,1004 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { uniq } from 'lodash'; +import * as minimatch from 'minimatch'; +import { + CancellationToken, + TestController, + TestItem, + TestRunRequest, + tests, + WorkspaceFolder, + RelativePattern, + TestRunProfileKind, + CancellationTokenSource, + Uri, + EventEmitter, + TextDocument, + FileCoverageDetail, + TestRun, + MarkdownString, +} from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager, IWorkspaceService } from '../../common/application/types'; +import * as constants from '../../common/constants'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; +import { noop } from '../../common/utils/misc'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; +import { TestProvider } from '../types'; +import { createErrorTestItem, DebugTestTag, getNodeByUri, RunTestTag } from './common/testItemUtilities'; +import { buildErrorNodeOptions } from './common/utils'; +import { ITestController, ITestFrameworkController, TestRefreshOptions } from './common/types'; +import { WorkspaceTestAdapter } from './workspaceTestAdapter'; +import { ITestDebugLauncher } from '../common/types'; +import { PythonResultResolver } from './common/resultResolver'; +import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { ProjectAdapter } from './common/projectAdapter'; +import { TestProjectRegistry } from './common/testProjectRegistry'; +import { createTestAdapters, getProjectId } from './common/projectUtils'; +import { executeTestsForProjects } from './common/projectTestExecution'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { DidChangePythonProjectsEventArgs, PythonProject } from '../../envExt/types'; + +// Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. +type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; +type TriggerKeyType = keyof EventPropertyType; +type TriggerType = EventPropertyType[TriggerKeyType]; + +@injectable() +export class PythonTestController implements ITestController, IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + // Legacy: Single workspace test adapter per workspace (backward compatibility) + private readonly testAdapters: Map = new Map(); + + // Registry for multi-project testing (one registry instance manages all projects across workspaces) + private readonly projectRegistry: TestProjectRegistry; + + private readonly triggerTypes: TriggerType[] = []; + + private readonly testController: TestController; + + private readonly refreshData: IDelayedTrigger; + + private refreshCancellation: CancellationTokenSource; + + private readonly refreshingCompletedEvent: EventEmitter = new EventEmitter(); + + private readonly refreshingStartedEvent: EventEmitter = new EventEmitter(); + + private readonly runWithoutConfigurationEvent: EventEmitter = new EventEmitter< + WorkspaceFolder[] + >(); + + public readonly onRefreshingCompleted = this.refreshingCompletedEvent.event; + + public readonly onRefreshingStarted = this.refreshingStartedEvent.event; + + public readonly onRunWithoutConfiguration = this.runWithoutConfigurationEvent.event; + + private sendTestDisabledTelemetry = true; + + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configSettings: IConfigurationService, + @inject(ITestFrameworkController) @named(PYTEST_PROVIDER) private readonly pytest: ITestFrameworkController, + @inject(ITestFrameworkController) @named(UNITTEST_PROVIDER) private readonly unittest: ITestFrameworkController, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, + @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, + ) { + this.refreshCancellation = new CancellationTokenSource(); + + this.testController = tests.createTestController('python-tests', 'Python Tests'); + this.disposables.push(this.testController); + + // Initialize project registry for multi-project testing support + this.projectRegistry = new TestProjectRegistry( + this.testController, + this.configSettings, + this.interpreterService, + this.envVarsService, + ); + + const delayTrigger = new DelayedTrigger( + (uri: Uri, invalidate: boolean) => { + this.refreshTestDataInternal(uri); + if (invalidate) { + this.invalidateTests(uri); + } + }, + 250, // Delay running the refresh by 250 ms + 'Refresh Test Data', + ); + this.disposables.push(delayTrigger); + this.refreshData = delayTrigger; + + this.disposables.push( + this.testController.createRunProfile( + 'Run Tests', + TestRunProfileKind.Run, + this.runTests.bind(this), + true, + RunTestTag, + ), + this.testController.createRunProfile( + 'Debug Tests', + TestRunProfileKind.Debug, + this.runTests.bind(this), + true, + DebugTestTag, + ), + this.testController.createRunProfile( + 'Coverage Tests', + TestRunProfileKind.Coverage, + this.runTests.bind(this), + true, + RunTestTag, + ), + ); + + this.testController.resolveHandler = this.resolveChildren.bind(this); + this.testController.refreshHandler = (token: CancellationToken) => { + this.disposables.push( + token.onCancellationRequested(() => { + traceVerbose('Testing: Stop refreshing triggered'); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERING_STOP); + this.stopRefreshing(); + }), + ); + + traceVerbose('Testing: Manually triggered test refresh'); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { + trigger: constants.CommandSource.commandPalette, + }); + return this.refreshTestData(undefined, { forceRefresh: true }); + }; + } + + /** + * Determines the test provider (pytest or unittest) based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : PYTEST_PROVIDER; + } + + /** + * Sets up file watchers for test discovery triggers. + */ + private setupFileWatchers(workspace: WorkspaceFolder): void { + const settings = this.configSettings.getSettings(workspace.uri); + if (settings.testing.autoTestDiscoverOnSaveEnabled) { + traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); + this.watchForSettingsChanges(workspace); + this.watchForTestContentChangeOnSave(); + } + } + + /** + * Activates the test controller for all workspaces. + * + * Two activation modes: + * 1. **Project-based mode** (when Python Environments API available): + * 2. **Legacy mode** (fallback): + * + * Uses `Promise.allSettled` for resilient multi-workspace activation: + */ + public async activate(): Promise { + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + // PROJECT-BASED MODE: Uses Python Environments API to discover projects + // Each project becomes its own test tree root with its own Python environment + if (useEnvExtension()) { + traceInfo('[test-by-project] Activating project-based testing mode'); + + // Discover projects in parallel across all workspaces + // Promise.allSettled ensures one workspace failure doesn't block others + const results = await Promise.allSettled( + Array.from(workspaces).map(async (workspace) => { + // Queries Python Environments API and creates ProjectAdapter instances + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspace.uri); + return { workspace, projectCount: projects.length }; + }), + ); + + // Process results: successful workspaces get file watchers, failed ones fall back to legacy + results.forEach((result, index) => { + const workspace = workspaces[index]; + if (result.status === 'fulfilled') { + traceInfo( + `[test-by-project] Activated ${result.value.projectCount} project(s) for ${workspace.uri.fsPath}`, + ); + this.setupFileWatchers(workspace); + } else { + // Graceful degradation: if project discovery fails, use legacy single-adapter mode + traceError(`[test-by-project] Failed for ${workspace.uri.fsPath}:`, result.reason); + this.activateLegacyWorkspace(workspace); + } + }); + // Subscribe to project changes to update test tree when projects are added/removed + await this.subscribeToProjectChanges(); + return; + } + + // LEGACY MODE: Single WorkspaceTestAdapter per workspace (backward compatibility) + workspaces.forEach((workspace) => { + this.activateLegacyWorkspace(workspace); + }); + } + + /** + * Subscribes to Python project changes from the Python Environments API. + * When projects are added or removed, updates the test tree accordingly. + */ + private async subscribeToProjectChanges(): Promise { + try { + const envExtApi = await getEnvExtApi(); + this.disposables.push( + envExtApi.onDidChangePythonProjects((event: DidChangePythonProjectsEventArgs) => { + this.handleProjectChanges(event).catch((error) => { + traceError('[test-by-project] Error handling project changes:', error); + }); + }), + ); + traceInfo('[test-by-project] Subscribed to Python project changes'); + } catch (error) { + traceError('[test-by-project] Failed to subscribe to project changes:', error); + } + } + + /** + * Handles changes to Python projects (added or removed). + * Cleans up stale test items and re-discovers projects and tests for affected workspaces. + */ + private async handleProjectChanges(event: DidChangePythonProjectsEventArgs): Promise { + const { added, removed } = event; + + if (added.length === 0 && removed.length === 0) { + return; + } + + traceInfo(`[test-by-project] Project changes detected: ${added.length} added, ${removed.length} removed`); + + // Find all affected workspaces + const affectedWorkspaces = new Set(); + + const findWorkspace = (project: PythonProject): WorkspaceFolder | undefined => { + return this.workspaceService.getWorkspaceFolder(project.uri); + }; + + for (const project of [...added, ...removed]) { + const workspace = findWorkspace(project); + if (workspace) { + affectedWorkspaces.add(workspace); + } + } + + // For each affected workspace, clean up and re-discover + for (const workspace of affectedWorkspaces) { + traceInfo(`[test-by-project] Re-discovering projects for workspace: ${workspace.uri.fsPath}`); + + // Get the current projects before clearing to know what to clean up + const existingProjects = this.projectRegistry.getProjectsArray(workspace.uri); + + // Remove ALL test items for the affected workspace's projects + // This ensures no stale items remain from deleted/changed projects + this.removeWorkspaceProjectTestItems(workspace.uri, existingProjects); + + // Also explicitly remove test items for removed projects (in case they weren't tracked) + for (const project of removed) { + const projectWorkspace = findWorkspace(project); + if (projectWorkspace?.uri.toString() === workspace.uri.toString()) { + this.removeProjectTestItems(project); + } + } + + // Re-discover all projects and tests for the workspace in a single pass. + // discoverAllProjectsInWorkspace is responsible for clearing/re-registering + // projects and performing test discovery for the workspace. + await this.discoverAllProjectsInWorkspace(workspace.uri); + } + } + + /** + * Removes all test items associated with projects in a workspace. + * Used to clean up stale items before re-discovery. + */ + private removeWorkspaceProjectTestItems(workspaceUri: Uri, projects: ProjectAdapter[]): void { + const idsToRemove: string[] = []; + + // Collect IDs of test items belonging to any project in this workspace + for (const project of projects) { + const projectIdPrefix = getProjectId(project.projectUri); + const projectFsPath = project.projectUri.fsPath; + + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectIdPrefix)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (legacy items might use path directly) + else if (item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + } + + // Also remove any items whose URI is within the workspace (catch-all for edge cases) + this.testController.items.forEach((item) => { + if ( + item.uri && + this.workspaceService.getWorkspaceFolder(item.uri)?.uri.toString() === workspaceUri.toString() + ) { + if (!idsToRemove.includes(item.id)) { + idsToRemove.push(item.id); + } + } + }); + + // Remove all collected items + for (const id of idsToRemove) { + this.testController.items.delete(id); + } + + traceInfo( + `[test-by-project] Cleaned up ${idsToRemove.length} test items for workspace: ${workspaceUri.fsPath}`, + ); + } + + /** + * Removes test items associated with a specific project from the test controller. + * Matches items by project ID prefix, fsPath, or URI. + */ + private removeProjectTestItems(project: PythonProject): void { + const projectId = getProjectId(project.uri); + const projectFsPath = project.uri.fsPath; + const idsToRemove: string[] = []; + + // Find all root items that belong to this project + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectId)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (items might use path directly without URI prefix) + else if (item.id.startsWith(projectFsPath) || item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + + for (const id of idsToRemove) { + this.testController.items.delete(id); + traceVerbose(`[test-by-project] Removed test item: ${id}`); + } + + if (idsToRemove.length > 0) { + traceInfo(`[test-by-project] Removed ${idsToRemove.length} test items for project: ${project.name}`); + } + } + + /** + * Activates testing for a workspace using the legacy single-adapter approach. + * Used for backward compatibility when project-based testing is disabled or unavailable. + */ + private activateLegacyWorkspace(workspace: WorkspaceFolder): void { + const testProvider = this.getTestProvider(workspace.uri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + testProvider, + discoveryAdapter, + executionAdapter, + workspace.uri, + resultResolver, + ); + + this.testAdapters.set(workspace.uri, workspaceTestAdapter); + this.setupFileWatchers(workspace); + } + + public refreshTestData(uri?: Resource, options?: TestRefreshOptions): Promise { + if (options?.forceRefresh) { + if (uri === undefined) { + // This is a special case where we want everything to be re-discovered. + traceVerbose('Testing: Clearing all discovered tests'); + this.testController.items.forEach((item) => { + const ids: string[] = []; + item.children.forEach((child) => ids.push(child.id)); + ids.forEach((id) => item.children.delete(id)); + }); + + traceVerbose('Testing: Forcing test data refresh'); + return this.refreshTestDataInternal(undefined); + } + + traceVerbose('Testing: Forcing test data refresh'); + return this.refreshTestDataInternal(uri); + } + + this.refreshData.trigger(uri, false); + return Promise.resolve(); + } + + public stopRefreshing(): void { + this.refreshCancellation.cancel(); + this.refreshCancellation.dispose(); + this.refreshCancellation = new CancellationTokenSource(); + } + + public clearTestController(): void { + const ids: string[] = []; + this.testController.items.forEach((item) => ids.push(item.id)); + ids.forEach((id) => this.testController.items.delete(id)); + } + + private async refreshTestDataInternal(uri?: Resource): Promise { + this.refreshingStartedEvent.fire(); + try { + if (uri) { + await this.discoverTestsInWorkspace(uri); + } else { + await this.discoverTestsInAllWorkspaces(); + } + } finally { + this.refreshingCompletedEvent.fire(); + } + } + + /** + * Discovers tests for a single workspace. + * + * **Discovery flow:** + * 1. If the workspace has registered projects (via Python Environments API), + * uses project-based discovery: each project is discovered independently + * with its own Python environment and test adapters. + * 2. Otherwise, falls back to legacy mode: a single WorkspaceTestAdapter + * discovers all tests in the workspace using the active interpreter. + * + * In project-based mode, the test tree will have separate roots for each project. + * In legacy mode, the workspace folder is the single test tree root. + */ + private async discoverTestsInWorkspace(uri: Uri): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(uri); + if (!workspace?.uri) { + traceError('Unable to find workspace for given file'); + return; + } + + const settings = this.configSettings.getSettings(uri); + traceVerbose(`Discover tests for workspace name: ${workspace.name} - uri: ${uri.fsPath}`); + + // Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; + + // Check if any test framework is enabled BEFORE project-based discovery + // This ensures the config screen stays visible when testing is disabled + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + await this.handleNoTestProviderEnabled(workspace); + return; + } + + // Use project-based discovery if applicable (only reached if testing is enabled) + if (this.projectRegistry.hasProjects(workspace.uri)) { + await this.discoverAllProjectsInWorkspace(workspace.uri); + return; + } + + // Legacy mode: Single workspace adapter + if (settings.testing.pytestEnabled) { + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'pytest'); + } else if (settings.testing.unittestEnabled) { + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'unittest'); + } + } + + /** + * Discovers tests for all projects within a workspace (project-based mode). + * Re-discovers projects from the Python Environments API before running test discovery. + * This ensures the test tree stays in sync with project changes. + */ + private async discoverAllProjectsInWorkspace(workspaceUri: Uri): Promise { + // Defensive check: ensure testing is enabled (should be checked by caller, but be safe) + const settings = this.configSettings.getSettings(workspaceUri); + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + traceVerbose('[test-by-project] Skipping discovery - no test framework enabled'); + return; + } + + // Get existing projects before re-discovery for cleanup + const existingProjects = this.projectRegistry.getProjectsArray(workspaceUri); + + // Clean up all existing test items for this workspace + // This ensures stale items from deleted/changed projects are removed + this.removeWorkspaceProjectTestItems(workspaceUri, existingProjects); + + // Re-discover projects from Python Environments API + // This picks up any added/removed projects since last discovery + this.projectRegistry.clearWorkspace(workspaceUri); + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspaceUri); + + if (projects.length === 0) { + traceError(`[test-by-project] No projects found for workspace: ${workspaceUri.fsPath}`); + return; + } + + traceInfo(`[test-by-project] Starting discovery for ${projects.length} project(s) in workspace`); + + try { + // Configure nested project exclusions before discovery + this.projectRegistry.configureNestedProjectIgnores(workspaceUri); + + // Track completion for progress logging + const projectsCompleted = new Set(); + + // Run discovery for all projects in parallel + await Promise.all(projects.map((project) => this.discoverTestsForProject(project, projectsCompleted))); + + traceInfo( + `[test-by-project] Discovery complete: ${projectsCompleted.size}/${projects.length} projects completed`, + ); + } catch (error) { + traceError(`[test-by-project] Discovery failed for workspace ${workspaceUri.fsPath}:`, error); + } + } + + /** + * Discovers tests for a single project (project-based mode). + * Creates test tree items rooted at the project's directory. + */ + private async discoverTestsForProject(project: ProjectAdapter, projectsCompleted: Set): Promise { + try { + traceInfo(`[test-by-project] Discovering tests for project: ${project.projectName}`); + project.isDiscovering = true; + + // In project-based mode, the discovery adapter uses the Python Environments API + // to get the environment directly, so we don't need to pass the interpreter + await project.discoveryAdapter.discoverTests( + project.projectUri, + this.pythonExecFactory, + this.refreshCancellation.token, + undefined, // Interpreter not needed; adapter uses Python Environments API + project, + ); + + // Mark project as completed (use URI string as unique key) + projectsCompleted.add(project.projectUri.toString()); + traceInfo(`[test-by-project] Project ${project.projectName} discovery completed`); + } catch (error) { + traceError(`[test-by-project] Discovery failed for project ${project.projectName}:`, error); + // Individual project failures don't block others + projectsCompleted.add(project.projectUri.toString()); // Still mark as completed + } finally { + project.isDiscovering = false; + } + } + + /** + * Discovers tests across all workspace folders. + * Iterates each workspace and triggers discovery. + */ + private async discoverTestsInAllWorkspaces(): Promise { + traceVerbose('Testing: Refreshing all test data'); + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + await Promise.all( + workspaces.map(async (workspace) => { + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } + } + await this.discoverTestsInWorkspace(workspace.uri); + }), + ); + } + + /** + * Discovers tests for a workspace using legacy single-adapter mode. + */ + private async discoverWorkspaceTestsLegacy(workspaceUri: Uri, expectedProvider: TestProvider): Promise { + const testAdapter = this.testAdapters.get(workspaceUri); + + if (!testAdapter) { + traceError('Unable to find test adapter for workspace.'); + return; + } + + const actualProvider = testAdapter.getTestProvider(); + if (actualProvider !== expectedProvider) { + traceError(`Test provider in adapter is not ${expectedProvider}. Please reload window.`); + this.surfaceErrorNode( + workspaceUri, + 'Test provider types are not aligned, please reload your VS Code window.', + expectedProvider, + ); + return; + } + + await testAdapter.discoverTests( + this.testController, + this.pythonExecFactory, + this.refreshCancellation.token, + await this.interpreterService.getActiveInterpreter(workspaceUri), + ); + } + + /** + * Handles the case when no test provider is enabled. + * Sends telemetry and removes test items for the workspace from the tree. + */ + private async handleNoTestProviderEnabled(workspace: WorkspaceFolder): Promise { + if (this.sendTestDisabledTelemetry) { + this.sendTestDisabledTelemetry = false; + sendTelemetryEvent(EventName.UNITTEST_DISABLED); + } + + this.removeTestItemsForWorkspace(workspace); + } + + /** + * Removes all test items belonging to a specific workspace from the test controller. + * This is used when test discovery is disabled for a workspace. + */ + private removeTestItemsForWorkspace(workspace: WorkspaceFolder): void { + const itemsToDelete: string[] = []; + + this.testController.items.forEach((testItem: TestItem) => { + const itemWorkspace = this.workspaceService.getWorkspaceFolder(testItem.uri); + if (itemWorkspace?.uri.fsPath === workspace.uri.fsPath) { + itemsToDelete.push(testItem.id); + } + }); + + itemsToDelete.forEach((id) => this.testController.items.delete(id)); + } + + private async resolveChildren(item: TestItem | undefined): Promise { + if (item) { + traceVerbose(`Testing: Resolving item ${item.id}`); + const settings = this.configSettings.getSettings(item.uri); + if (settings.testing.pytestEnabled) { + return this.pytest.resolveChildren(this.testController, item, this.refreshCancellation.token); + } + if (settings.testing.unittestEnabled) { + return this.unittest.resolveChildren(this.testController, item, this.refreshCancellation.token); + } + } else { + traceVerbose('Testing: Refreshing all test data'); + this.sendTriggerTelemetry('auto'); + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + await Promise.all( + workspaces.map(async (workspace) => { + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + traceError('Cannot trigger test discovery as a valid interpreter is not selected'); + return; + } + } + await this.refreshTestDataInternal(workspace.uri); + }), + ); + } + return Promise.resolve(); + } + + private async runTests(request: TestRunRequest, token: CancellationToken): Promise { + const workspaces = this.getWorkspacesForTestRun(request); + const runInstance = this.testController.createTestRun( + request, + `Running Tests for Workspace(s): ${workspaces.map((w) => w.uri.fsPath).join(';')}`, + true, + ); + + const dispose = token.onCancellationRequested(() => { + runInstance.appendOutput(`\nRun instance cancelled.\r\n`); + runInstance.end(); + }); + + const unconfiguredWorkspaces: WorkspaceFolder[] = []; + + try { + await Promise.all( + workspaces.map((workspace) => + this.runTestsForWorkspace(workspace, request, runInstance, token, unconfiguredWorkspaces), + ), + ); + } finally { + traceVerbose('Finished running tests, ending runInstance.'); + runInstance.appendOutput(`Finished running tests!\r\n`); + runInstance.end(); + dispose.dispose(); + if (unconfiguredWorkspaces.length > 0) { + this.runWithoutConfigurationEvent.fire(unconfiguredWorkspaces); + } + } + } + + /** + * Gets the list of workspaces to run tests for based on the test run request. + */ + private getWorkspacesForTestRun(request: TestRunRequest): WorkspaceFolder[] { + if (request.include) { + const workspaces: WorkspaceFolder[] = []; + uniq(request.include.map((r) => this.workspaceService.getWorkspaceFolder(r.uri))).forEach((w) => { + if (w) { + workspaces.push(w); + } + }); + return workspaces; + } + return Array.from(this.workspaceService.workspaceFolders || []); + } + + /** + * Runs tests for a single workspace. + */ + private async runTestsForWorkspace( + workspace: WorkspaceFolder, + request: TestRunRequest, + runInstance: TestRun, + token: CancellationToken, + unconfiguredWorkspaces: WorkspaceFolder[], + ): Promise { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } + + const testItems = this.getTestItemsForWorkspace(workspace, request); + const settings = this.configSettings.getSettings(workspace.uri); + + if (testItems.length === 0) { + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + unconfiguredWorkspaces.push(workspace); + } + return; + } + + // Check if we're in project-based mode and should use project-specific execution + if (this.projectRegistry.hasProjects(workspace.uri)) { + const projects = this.projectRegistry.getProjectsArray(workspace.uri); + await executeTestsForProjects(projects, testItems, runInstance, request, token, { + projectRegistry: this.projectRegistry, + pythonExecFactory: this.pythonExecFactory, + debugLauncher: this.debugLauncher, + }); + return; + } + + // For unittest (or pytest when not in project mode), use the legacy WorkspaceTestAdapter. + // In project mode, legacy adapters may not be initialized, so create one on demand. + let testAdapter = this.testAdapters.get(workspace.uri); + if (!testAdapter) { + // Initialize legacy adapter on demand (needed for unittest in project mode) + this.activateLegacyWorkspace(workspace); + testAdapter = this.testAdapters.get(workspace.uri); + } + + if (!testAdapter) { + traceError(`[test] No test adapter available for workspace: ${workspace.uri.fsPath}`); + return; + } + + this.setupCoverageIfNeeded(request, testAdapter); + + if (settings.testing.pytestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'pytest', + ); + } else if (settings.testing.unittestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'unittest', + ); + } else { + unconfiguredWorkspaces.push(workspace); + } + } + + /** + * Gets test items that belong to a specific workspace from the run request. + */ + private getTestItemsForWorkspace(workspace: WorkspaceFolder, request: TestRunRequest): TestItem[] { + const testItems: TestItem[] = []; + // If the run request includes test items then collect only items that belong to + // `workspace`. If there are no items in the run request then just run the `workspace` + // root test node. Include will be `undefined` in the "run all" scenario. + (request.include ?? this.testController.items).forEach((i: TestItem) => { + const w = this.workspaceService.getWorkspaceFolder(i.uri); + if (w?.uri.fsPath === workspace.uri.fsPath) { + testItems.push(i); + } + }); + return testItems; + } + + /** + * Sets up detailed coverage loading if the run profile is for coverage. + */ + private setupCoverageIfNeeded(request: TestRunRequest, testAdapter: WorkspaceTestAdapter): void { + // no profile will have TestRunProfileKind.Coverage if rewrite isn't enabled + if (request.profile?.kind && request.profile?.kind === TestRunProfileKind.Coverage) { + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const details = testAdapter.resultResolver.detailedCoverageMap.get(fileCoverage.uri.fsPath); + if (details === undefined) { + // given file has no detailed coverage data + return Promise.resolve([]); + } + return Promise.resolve(details); + }; + } + } + + /** + * Executes tests using the test adapter for a specific test provider. + */ + private async executeTestsForProvider( + workspace: WorkspaceFolder, + testAdapter: WorkspaceTestAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + provider: TestProvider, + ): Promise { + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: provider, + debugging: request.profile?.kind === TestRunProfileKind.Debug, + }); + + await testAdapter.executeTests( + this.testController, + runInstance, + testItems, + this.pythonExecFactory, + token, + request.profile?.kind, + this.debugLauncher, + await this.interpreterService.getActiveInterpreter(workspace.uri), + ); + } + + private invalidateTests(uri: Uri) { + this.testController.items.forEach((root) => { + const item = getNodeByUri(root, uri); + if (item && !!item.invalidateResults) { + // Minimize invalidating to test case nodes for the test file where + // the change occurred + item.invalidateResults(); + } + }); + } + + private watchForSettingsChanges(workspace: WorkspaceFolder): void { + const pattern = new RelativePattern(workspace, '**/{settings.json,pytest.ini,pyproject.toml,setup.cfg}'); + const watcher = this.workspaceService.createFileSystemWatcher(pattern); + this.disposables.push(watcher); + + this.disposables.push( + onDidSaveTextDocument(async (doc: TextDocument) => { + const file = doc.fileName; + // refresh on any settings file save + if ( + file.includes('settings.json') || + file.includes('pytest.ini') || + file.includes('setup.cfg') || + file.includes('pyproject.toml') + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } + }), + ); + /* Keep both watchers for create and delete since config files can change test behavior without content + due to their impact on pythonPath. */ + this.disposables.push( + watcher.onDidCreate((uri) => { + traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(uri, false); + }), + ); + this.disposables.push( + watcher.onDidDelete((uri) => { + traceVerbose(`Testing: Trigger refresh after deleting in ${uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(uri, false); + }), + ); + } + + private watchForTestContentChangeOnSave(): void { + this.disposables.push( + onDidSaveTextDocument(async (doc: TextDocument) => { + const settings = this.configSettings.getSettings(doc.uri); + if ( + settings.testing.autoTestDiscoverOnSaveEnabled && + minimatch.default(doc.uri.fsPath, settings.testing.autoTestDiscoverOnSavePattern) + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } + }), + ); + } + + /** + * Send UNITTEST_DISCOVERY_TRIGGER telemetry event only once per trigger type. + * + * @param triggerType The trigger type to send telemetry for. + */ + private sendTriggerTelemetry(trigger: TriggerType): void { + if (!this.triggerTypes.includes(trigger)) { + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { + trigger, + }); + this.triggerTypes.push(trigger); + } + } + + private surfaceErrorNode(workspaceUri: Uri, message: string, testProvider: TestProvider): void { + let errorNode = this.testController.items.get(`DiscoveryError:${workspaceUri.fsPath}`); + if (errorNode === undefined) { + const options = buildErrorNodeOptions(workspaceUri, message, testProvider); + errorNode = createErrorTestItem(this.testController, options); + this.testController.items.add(errorNode); + } + const errorNodeLabel: MarkdownString = new MarkdownString(message); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; + } +} diff --git a/src/client/testing/testController/pytest/arguments.ts b/src/client/testing/testController/pytest/arguments.ts new file mode 100644 index 000000000000..2b4efbd56f42 --- /dev/null +++ b/src/client/testing/testController/pytest/arguments.ts @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestFilter } from '../../common/types'; +import { getPositionalArguments, filterArguments } from '../common/argumentsHelper'; + +const OptionsWithArguments = [ + '-c', + '-k', + '-m', + '-o', + '-p', + '-r', + '-W', + '-n', // -n is a pytest-xdist option + '--assert', + '--basetemp', + '--cache-show', + '--capture', + '--code-highlight', + '--color', + '--confcutdir', + '--cov', + '--cov-config', + '--cov-fail-under', + '--cov-report', + '--deselect', + '--dist', + '--doctest-glob', + '--doctest-report', + '--durations', + '--durations-min', + '--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-header', + '--no-print-logs', + '--no-summary', + '--noconftest', + '--old-summary', + '--pdb', + '--pyargs', + '-PyTest, Unittest-pyargs', + '--quiet', + '--runxfail', + '--setup-only', + '--setup-plan', + '--setup-show', + '--showlocals', + '--stepwise', + '--sw', + '--stepwise-skip', + '--strict', + '--strict-config', + '--strict-markers', + '--trace-config', + '--verbose', + '--version', + '-V', + '-h', + '-l', + '-q', + '-s', + '-v', + '-x', + '--boxed', + '--forked', + '--looponfail', + '--trace', + '--tx', + '-d', +]; + +export function removePositionalFoldersAndFiles(args: string[]): string[] { + return pytestFilterArguments(args, TestFilter.removeTests); +} + +function pytestFilterArguments(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', + '--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 = getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); + filteredArgs = filteredArgs.filter((item) => positionalArgs.indexOf(item) === -1); + } + return filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); +} diff --git a/src/client/testing/testController/pytest/pytestController.ts b/src/client/testing/testController/pytest/pytestController.ts new file mode 100644 index 000000000000..f75580c11236 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestController.ts @@ -0,0 +1,142 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, TestItem, Uri, TestController } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { asyncForEach } from '../../../common/utils/arrayUtils'; +import { Deferred } from '../../../common/utils/async'; +import { + createWorkspaceRootTestItem, + getWorkspaceNode, + removeItemByIdFromChildren, + updateTestItemFromRawData, +} from '../common/testItemUtilities'; +import { ITestFrameworkController, TestData, RawDiscoveredTests } from '../common/types'; + +@injectable() +export class PytestController implements ITestFrameworkController { + private readonly testData: Map = new Map(); + + private discovering: Map> = new Map(); + + private idToRawData: Map = new Map(); + + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + public async resolveChildren( + testController: TestController, + item: TestItem, + token?: CancellationToken, + ): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(item.uri); + if (workspace) { + // if we are still discovering then wait + const discovery = this.discovering.get(workspace.uri.fsPath); + if (discovery) { + await discovery.promise; + } + + // see if we have raw test data + const rawTestData = this.testData.get(workspace.uri.fsPath); + if (rawTestData) { + // Refresh each node with new data + if (rawTestData.length === 0) { + const items: TestItem[] = []; + testController.items.forEach((i) => items.push(i)); + items.forEach((i) => testController.items.delete(i.id)); + return Promise.resolve(); + } + + const root = rawTestData.length === 1 ? rawTestData[0].root : workspace.uri.fsPath; + if (root === item.id) { + // This is the workspace root node + if (rawTestData.length === 1) { + if (rawTestData[0].tests.length > 0) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + item.id, + rawTestData, + token, + ); + } else { + this.idToRawData.delete(item.id); + testController.items.delete(item.id); + return Promise.resolve(); + } + } else { + // To figure out which top level nodes have to removed. First we get all the + // existing nodes. Then if they have data we keep those nodes, Nodes without + // data will be removed after we check the raw data. + let subRootWithNoData: string[] = []; + item.children.forEach((c) => subRootWithNoData.push(c.id)); + + await asyncForEach(rawTestData, async (data) => { + let subRootId = data.root; + let rawId; + if (data.root === root) { + const subRoot = data.parents.filter((p) => p.parentid === '.' || p.parentid === root); + subRootId = path.join(data.root, subRoot.length > 0 ? subRoot[0].id : ''); + rawId = subRoot.length > 0 ? subRoot[0].id : undefined; + } + + if (data.tests.length > 0) { + let subRootItem = item.children.get(subRootId); + if (!subRootItem) { + subRootItem = createWorkspaceRootTestItem(testController, this.idToRawData, { + id: subRootId, + label: path.basename(subRootId), + uri: Uri.file(subRootId), + runId: subRootId, + parentId: item.id, + rawId, + }); + item.children.add(subRootItem); + } + + // We found data for a node. Remove its id from the no-data list. + subRootWithNoData = subRootWithNoData.filter((s) => s !== subRootId); + await updateTestItemFromRawData( + subRootItem, + testController, + this.idToRawData, + root, // All the file paths are based on workspace root. + [data], + token, + ); + } else { + // This means there are no tests under this node + removeItemByIdFromChildren(this.idToRawData, item, [subRootId]); + } + }); + + // We did not find any data for these nodes, delete them. + removeItemByIdFromChildren(this.idToRawData, item, subRootWithNoData); + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + workspaceNode.id, + rawTestData, + token, + ); + } + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + testController.items.delete(workspaceNode.id); + } + } + } + return Promise.resolve(); + } +} diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts new file mode 100644 index 000000000000..16e27635e66c --- /dev/null +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { CancellationToken, Disposable, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred } from '../../../common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { createTestingDeferred } from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { useEnvExtension, getEnvironment, runInBackground } from '../../../envExt/api.internal'; +import { buildPytestEnv as configureSubprocessEnv, handleSymlinkAndRootDir } from './pytestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; + +/** + * Configures the subprocess environment for pytest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess + */ +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, fullPluginPath, discoveryPipeName); + return mutableEnv; +} + +/** + * Wrapper class for pytest test discovery. This is where we call the pytest subprocess. + */ +export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); + + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose: Deferred = createTestingDeferred(); + + // Collect all disposables related to discovery to handle cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); + } + + try { + // Build pytest command and arguments + const settings = this.configSettings.getSettings(uri); + let { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + pytestArgs = await handleSymlinkAndRootDir(cwd, pytestArgs); + + // Add --ignore flags for nested projects to prevent duplicate discovery + if (project?.nestedProjectPathsToIgnore?.length) { + const ignoreArgs = project.nestedProjectPathsToIgnore.map((nestedPath) => `--ignore=${nestedPath}`); + pytestArgs = [...pytestArgs, ...ignoreArgs]; + traceInfo( + `[test-by-project] Project ${project.projectName} ignoring nested project(s): ${ignoreArgs.join( + ' ', + )}`, + ); + } + + const commandArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose( + `Running pytest discovery with command: ${commandArgs.join(' ')} for workspace ${uri.fsPath}.`, + ); + + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + } + + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('pytest', uri, cwd, this.resultResolver, deferredTillExecClose, [5]); + + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for pytest discovery in workspace ${uri.fsPath}`); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: commandArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + traceInfo(`Started pytest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); + + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('pytest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); + proc.onExit((code, signal) => { + handlers.onExit(code, signal); + handlers.onClose(code, signal); + }); + + await deferredTillExecClose.promise; + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + return; + } + + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for pytest discovery in workspace ${uri.fsPath}`); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); + + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Pytest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; + + let resultProc: ChildProcess | undefined; + + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during pytest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('pytest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (cancellationHandler) { + disposables.push(cancellationHandler); + } + + try { + const result = execService.execObservable(commandArgs, spawnOptions); + resultProc = result?.proc; + + if (!resultProc) { + traceError(`Failed to spawn pytest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + traceInfo(`Started pytest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning pytest discovery subprocess for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for pytest discovery subprocess to complete for workspace ${uri.fsPath}`); + await deferredTillExecClose.promise; + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during pytest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); + } + } +} diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts new file mode 100644 index 000000000000..102841c2e2dd --- /dev/null +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as path from 'path'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { ExecutionTestPayload, ITestExecutionAdapter, ITestResultResolver } from '../common/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { removePositionalFoldersAndFiles } from './arguments'; +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { PYTEST_PROVIDER } from '../../common/constants'; +import { EXTENSION_ROOT_DIR } from '../../../common/constants'; +import * as utils from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from '../common/projectAdapter'; + +export class PytestTestExecutionAdapter implements ITestExecutionAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async runTests( + uri: Uri, + testIds: string[], + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + const deferredTillServerClose: Deferred = utils.createTestingDeferred(); + + // create callback to handle data received on the named pipe + const dataReceivedCallback = (data: ExecutionTestPayload) => { + if (runInstance && !runInstance.token.isCancellationRequested) { + this.resultResolver?.resolveExecution(data, runInstance); + } else { + traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); + } + }; + const cSource = new CancellationTokenSource(); + runInstance.token.onCancellationRequested(() => cSource.cancel()); + + const name = await utils.startRunResultNamedPipe( + dataReceivedCallback, // callback to handle data received + deferredTillServerClose, // deferred to resolve when server closes + cSource.token, // token to cancel + ); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); + }); + + try { + await this.runTestsNew( + uri, + testIds, + name, + cSource, + runInstance, + profileKind, + executionFactory, + debugLauncher, + interpreter, + project, + ); + } finally { + await deferredTillServerClose.promise; + } + } + + private async runTestsNew( + uri: Uri, + testIds: string[], + resultNamedPipeName: string, + serverCancel: CancellationTokenSource, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + const relativePathToPytest = 'python_files'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + // get and edit env vars + const mutableEnv = { + ...(await this.envVarsService?.getEnvironmentVariables(uri)), + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo(`[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for pytest execution`); + } + + if (profileKind && profileKind === TestRunProfileKind.Coverage) { + mutableEnv.COVERAGE_ENABLED = 'True'; + } + + const debugBool = profileKind && profileKind === TestRunProfileKind.Debug; + + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + // need to check what will happen in the exec service is NOT defined and is null + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for pytest execution: ${execInfo}.`); + + try { + // Remove positional test folders and files, we will add as needed per node + let testArgs = removePositionalFoldersAndFiles(pytestArgs); + + // if user has provided `--rootdir` then use that, otherwise add `cwd` + // root dir is required so pytest can find the relative paths and for symlinks + utils.addValueIfKeyNotExist(testArgs, '--rootdir', cwd); + + // -s and --capture are both command line options that control how pytest captures output. + // if neither are set, then set --capture=no to prevent pytest from capturing output. + if (debugBool && !utils.argKeyExists(testArgs, '-s')) { + testArgs = utils.addValueIfKeyNotExist(testArgs, '--capture', 'no'); + } + + // create a file with the test ids and set the environment variable to the file name + const testIdsFileName = await utils.writeTestIdsFile(testIds); + mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; + traceInfo( + `Environment variables set for pytest execution: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}, RUN_TEST_IDS_PIPE=${mutableEnv.RUN_TEST_IDS_PIPE}`, + ); + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token: runInstance.token, + }; + + if (debugBool) { + const launchOptions: LaunchOptions = { + cwd, + args: testArgs, + token: runInstance.token, + testProvider: PYTEST_PROVIDER, + runTestIdsPort: testIdsFileName, + pytestPort: resultNamedPipeName, + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, + }; + const sessionOptions: DebugSessionOptions = { + testRun: runInstance, + }; + traceInfo(`Running DEBUG pytest with arguments: ${testArgs} for workspace ${uri.fsPath} \r\n`); + await debugLauncher!.launchDebugger( + launchOptions, + () => { + serverCancel.cancel(); + }, + sessionOptions, + ); + } else if (useEnvExtension()) { + // For project-based execution, use the project's Python environment + // Otherwise, fall back to getting the environment from the URI + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (pythonEnv) { + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); + + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')} for workspace ${uri.fsPath} \r\n`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: runArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + proc.stdout.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.stderr.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.onExit((code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } else { + traceError(`Python Environment not found for: ${uri.fsPath}`); + } + } else { + // deferredTillExecClose is resolved when all stdout and stderr is read + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); + // combine path to run script with run args + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')} for workspace ${uri.fsPath} \r\n`); + + let resultProc: ChildProcess | undefined; + + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose.resolve(); + serverCancel.cancel(); + } + }); + + const result = execService?.execObservable(runArgs, spawnOptions); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + }); + + result?.proc?.on('close', (code, signal) => { + traceVerbose('Test run finished, subprocess closed.'); + // if the child has testIds then this is a run request + // if the child process exited with a non-zero exit code, then we need to send the error payload. + if (code !== 0) { + traceError( + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating and sending error execution payload \n`, + ); + + if (runInstance) { + this.resultResolver?.resolveExecution( + utils.createExecutionErrorPayload(code, signal, testIds, cwd), + runInstance, + ); + } + } + + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } + } catch (ex) { + traceError(`Error while running tests for workspace ${uri}: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); + } + + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; + return executionPayload; + } +} diff --git a/src/client/testing/testController/pytest/pytestHelpers.ts b/src/client/testing/testController/pytest/pytestHelpers.ts new file mode 100644 index 000000000000..c6e748fb85a7 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestHelpers.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import * as fs from 'fs'; +import { traceInfo, traceWarn } from '../../../logging'; +import { addValueIfKeyNotExist, hasSymlinkParent } from '../common/utils'; + +/** + * Checks if the current working directory contains a symlink and ensures --rootdir is set in pytest args. + * This is required for pytest to correctly resolve relative paths in symlinked directories. + */ +export async function handleSymlinkAndRootDir(cwd: string, pytestArgs: string[]): Promise { + const stats = await fs.promises.lstat(cwd); + const resolvedPath = await fs.promises.realpath(cwd); + let isSymbolicLink = false; + if (stats.isSymbolicLink()) { + isSymbolicLink = true; + traceWarn(`Working directory is a symbolic link: ${cwd} -> ${resolvedPath}`); + } else if (resolvedPath !== cwd) { + traceWarn( + `Working directory resolves to different path: ${cwd} -> ${resolvedPath}. Checking for symlinks in parent directories.`, + ); + isSymbolicLink = await hasSymlinkParent(cwd); + } + if (isSymbolicLink) { + traceWarn( + `Symlink detected in path. Adding '--rootdir=${cwd}' to pytest args to ensure correct path resolution.`, + ); + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + } + // if user has provided `--rootdir` then use that, otherwise add `cwd` + // root dir is required so pytest can find the relative paths and for symlinks + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + return pytestArgs; +} + +/** + * Builds the environment variables required for pytest discovery. + * Sets PYTHONPATH to include the plugin path and TEST_RUN_PIPE for communication. + */ +export function buildPytestEnv( + envVars: { [key: string]: string | undefined } | undefined, + fullPluginPath: string, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo( + `Environment variables set for pytest discovery: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`, + ); + return mutableEnv; +} diff --git a/src/client/testing/testController/serviceRegistry.ts b/src/client/testing/testController/serviceRegistry.ts new file mode 100644 index 000000000000..03bf883e8eb1 --- /dev/null +++ b/src/client/testing/testController/serviceRegistry.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IServiceManager } from '../../ioc/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; +import { ITestFrameworkController, ITestController } from './common/types'; +import { PythonTestController } from './controller'; +import { PytestController } from './pytest/pytestController'; +import { UnittestController } from './unittest/unittestController'; + +export function registerTestControllerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton(ITestFrameworkController, PytestController, PYTEST_PROVIDER); + + serviceManager.addSingleton( + ITestFrameworkController, + UnittestController, + UNITTEST_PROVIDER, + ); + serviceManager.addSingleton(ITestController, PythonTestController); + serviceManager.addBinding(ITestController, IExtensionSingleActivationService); +} diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts new file mode 100644 index 000000000000..558e01f3514d --- /dev/null +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Disposable, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { createTestingDeferred } from '../common/utils'; +import { buildDiscoveryCommand, buildUnittestEnv as configureSubprocessEnv } from './unittestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; + +/** + * Configures the subprocess environment for unittest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess + */ +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, discoveryPipeName); + return mutableEnv; +} + +/** + * Wrapper class for unittest test discovery. + */ +export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); + + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose = createTestingDeferred(); + + // Collect all disposables for cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); + } + try { + // Build unittest command and arguments + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + const execArgs = buildDiscoveryCommand(unittestArgs, EXTENSION_ROOT_DIR); + traceVerbose(`Running unittest discovery with command: ${execArgs.join(' ')} for workspace ${uri.fsPath}.`); + + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest discovery`, + ); + } + + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('unittest', uri, cwd, this.resultResolver, deferredTillExecClose); + + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for unittest discovery in workspace ${uri.fsPath}`); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: execArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + traceInfo(`Started unittest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); + + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('unittest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); + proc.onExit((code, signal) => { + handlers.onExit(code, signal); + handlers.onClose(code, signal); + }); + + await deferredTillExecClose.promise; + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + return; + } + + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for unittest discovery in workspace ${uri.fsPath}`); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); + + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Unittest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; + + let resultProc: ChildProcess | undefined; + + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during unittest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('unittest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (cancellationHandler) { + disposables.push(cancellationHandler); + } + + try { + const result = execService.execObservable(execArgs, spawnOptions); + resultProc = result?.proc; + + if (!resultProc) { + traceError(`Failed to spawn unittest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + traceInfo(`Started unittest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning unittest discovery subprocess for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for unittest discovery subprocess to complete for workspace ${uri.fsPath}`); + await deferredTillExecClose.promise; + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during unittest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + traceVerbose(`Cleaning up unittest discovery resources for workspace ${uri.fsPath}`); + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); + } + } +} diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts new file mode 100644 index 000000000000..c7d21b768c5b --- /dev/null +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { + ExecutionTestPayload, + ITestExecutionAdapter, + ITestResultResolver, + TestCommandOptions, + TestExecutionCommand, +} from '../common/types'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { fixLogLinesNoTrailing } from '../common/utils'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import * as utils from '../common/utils'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from '../common/projectAdapter'; + +/** + * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? + */ + +export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + public async runTests( + uri: Uri, + testIds: string[], + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + _interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // deferredTillServerClose awaits named pipe server close + const deferredTillServerClose: Deferred = utils.createTestingDeferred(); + + // create callback to handle data received on the named pipe + const dataReceivedCallback = (data: ExecutionTestPayload) => { + if (runInstance && !runInstance.token.isCancellationRequested) { + this.resultResolver?.resolveExecution(data, runInstance); + } else { + traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); + } + }; + const cSource = new CancellationTokenSource(); + runInstance.token.onCancellationRequested(() => cSource.cancel()); + const name = await utils.startRunResultNamedPipe( + dataReceivedCallback, // callback to handle data received + deferredTillServerClose, // deferred to resolve when server closes + cSource.token, // token to cancel + ); + runInstance.token.onCancellationRequested(() => { + console.log(`Test run cancelled, resolving 'till TillAllServerClose' deferred for ${uri.fsPath}.`); + // if canceled, stop listening for results + deferredTillServerClose.resolve(); + }); + try { + await this.runTestsNew( + uri, + testIds, + name, + cSource, + runInstance, + profileKind, + executionFactory, + debugLauncher, + project, + ); + } catch (error) { + traceError(`Error in running unittest tests: ${error}`); + } finally { + await deferredTillServerClose.promise; + } + } + + private async runTestsNew( + uri: Uri, + testIds: string[], + resultNamedPipeName: string, + serverCancel: CancellationTokenSource, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + project?: ProjectAdapter, + ): Promise { + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + + const command = buildExecutionCommand(unittestArgs); + let mutableEnv: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (mutableEnv === undefined) { + mutableEnv = {} as EnvironmentVariables; + } + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [cwd, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest execution`, + ); + } + + if (profileKind && profileKind === TestRunProfileKind.Coverage) { + mutableEnv.COVERAGE_ENABLED = cwd; + } + + const options: TestCommandOptions = { + workspaceFolder: uri, + command, + cwd, + profileKind: typeof profileKind === 'boolean' ? undefined : profileKind, + testIds, + token: runInstance.token, + }; + traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); + + // create named pipe server to send test ids + const testIdsFileName = await utils.writeTestIdsFile(testIds); + mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; + traceInfo( + `All environment variables set for unittest execution, PYTHONPATH: ${JSON.stringify( + mutableEnv.PYTHONPATH, + )}`, + ); + + const spawnOptions: SpawnOptions = { + token: options.token, + cwd: options.cwd, + throwOnStdErr: true, + env: mutableEnv, + }; + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: options.workspaceFolder, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for unittest execution: ${execInfo}.`); + + const args = [options.command.script].concat(options.command.args); + + if (options.outChannel) { + options.outChannel.appendLine(`python ${args.join(' ')}`); + } + + try { + if (options.profileKind && options.profileKind === TestRunProfileKind.Debug) { + const launchOptions: LaunchOptions = { + cwd: options.cwd, + args, + token: options.token, + testProvider: UNITTEST_PROVIDER, + runTestIdsPort: testIdsFileName, + pytestPort: resultNamedPipeName, // change this from pytest + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, + }; + const sessionOptions: DebugSessionOptions = { + testRun: runInstance, + }; + traceInfo(`Running DEBUG unittest for workspace ${options.cwd} with arguments: ${args}\r\n`); + + if (debugLauncher === undefined) { + traceError('Debug launcher is not defined'); + throw new Error('Debug launcher is not defined'); + } + await debugLauncher.launchDebugger( + launchOptions, + () => { + serverCancel.cancel(); + }, + sessionOptions, + ); + } else if (useEnvExtension()) { + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (pythonEnv) { + traceInfo(`Running unittest with arguments: ${args.join(' ')} for workspace ${uri.fsPath} \r\n`); + const deferredTillExecClose = createDeferred(); + + const proc = await runInBackground(pythonEnv, { + cwd, + args, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + proc.stdout.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.stderr.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.onExit((code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } else { + traceError(`Python Environment not found for: ${uri.fsPath}`); + } + } else { + // This means it is running the test + traceInfo(`Running unittests for workspace ${cwd} with arguments: ${args}\r\n`); + + const deferredTillExecClose = createDeferred>(); + + let resultProc: ChildProcess | undefined; + + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${cwd}.`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + serverCancel.cancel(); + } + }); + + const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; + + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(`${out}`); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(`${out}`); + }); + + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds) { + // This occurs when we are running the test and there is an error which occurs. + + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} for workspace ${options.cwd}. Creating and sending error execution payload \n`, + ); + if (runInstance) { + this.resultResolver?.resolveExecution( + utils.createExecutionErrorPayload(code, signal, testIds, cwd), + runInstance, + ); + } + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } + } catch (ex) { + traceError(`Error while running tests for workspace ${uri}: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); + } + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; + return executionPayload; + } +} + +function buildExecutionCommand(args: string[]): TestExecutionCommand { + const executionScript = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'execution.py'); + + return { + script: executionScript, + args: ['--udiscovery', ...args], + }; +} diff --git a/src/client/testing/testController/unittest/unittestController.ts b/src/client/testing/testController/unittest/unittestController.ts new file mode 100644 index 000000000000..863f34abd514 --- /dev/null +++ b/src/client/testing/testController/unittest/unittestController.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { CancellationToken, TestController, TestItem } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { Deferred } from '../../../common/utils/async'; +import { ITestFrameworkController, RawDiscoveredTests, TestData } from '../common/types'; +import { getWorkspaceNode, updateTestItemFromRawData } from '../common/testItemUtilities'; + +@injectable() +export class UnittestController implements ITestFrameworkController { + private readonly testData: Map = new Map(); + + private discovering: Map> = new Map(); + + private idToRawData: Map = new Map(); + + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + public async resolveChildren( + testController: TestController, + item: TestItem, + token?: CancellationToken, + ): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(item.uri); + if (workspace) { + // if we are still discovering then wait + const discovery = this.discovering.get(workspace.uri.fsPath); + if (discovery) { + await discovery.promise; + } + + // see if we have raw test data + const rawTestData = this.testData.get(workspace.uri.fsPath); + if (rawTestData) { + if (rawTestData.root === item.id) { + if (rawTestData.tests.length === 0) { + testController.items.delete(item.id); + return Promise.resolve(); + } + + if (rawTestData.tests.length > 0) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + item.id, + [rawTestData], + token, + ); + } else { + this.idToRawData.delete(item.id); + testController.items.delete(item.id); + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + workspaceNode.id, + [rawTestData], + token, + ); + } + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + testController.items.delete(workspaceNode.id); + } + } + } + return Promise.resolve(); + } +} diff --git a/src/client/testing/testController/unittest/unittestHelpers.ts b/src/client/testing/testController/unittest/unittestHelpers.ts new file mode 100644 index 000000000000..249a78dda7b7 --- /dev/null +++ b/src/client/testing/testController/unittest/unittestHelpers.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { traceInfo } from '../../../logging'; + +/** + * Builds the environment variables required for unittest discovery. + * Sets TEST_RUN_PIPE for communication. + */ +export function buildUnittestEnv( + envVars: { [key: string]: string | undefined } | undefined, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo(`Environment variables set for unittest discovery: TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`); + return mutableEnv; +} + +/** + * Builds the unittest discovery command. + */ +export function buildDiscoveryCommand(args: string[], extensionRootDir: string): string[] { + const discoveryScript = path.join(extensionRootDir, 'python_files', 'unittestadapter', 'discovery.py'); + return [discoveryScript, '--udiscovery', ...args]; +} diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts new file mode 100644 index 000000000000..f17687732f57 --- /dev/null +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as util from 'util'; +import { CancellationToken, TestController, TestItem, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { Testing } from '../../common/utils/localize'; +import { traceError } from '../../logging'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { TestProvider } from '../types'; +import { createErrorTestItem, getTestCaseNodes } from './common/testItemUtilities'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './common/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { ITestDebugLauncher } from '../common/types'; +import { buildErrorNodeOptions } from './common/utils'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ProjectAdapter } from './common/projectAdapter'; + +/** + * This class exposes a test-provider-agnostic way of discovering tests. + * + * It gets instantiated by the `PythonTestController` class in charge of reflecting test data in the UI, + * and then instantiates provider-specific adapters under the hood depending on settings. + * + * This class formats the JSON test data returned by the `[Unittest|Pytest]TestDiscoveryAdapter` into test UI elements, + * and uses them to insert/update/remove items in the `TestController` instance behind the testing UI whenever the `PythonTestController` requests a refresh. + */ +export class WorkspaceTestAdapter { + private discovering: Deferred | undefined; + + private executing: Deferred | undefined; + + constructor( + private testProvider: TestProvider, + private discoveryAdapter: ITestDiscoveryAdapter, + private executionAdapter: ITestExecutionAdapter, + private workspaceUri: Uri, + public resultResolver: ITestResultResolver, + ) {} + + public async executeTests( + testController: TestController, + runInstance: TestRun, + includes: TestItem[], + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + profileKind?: boolean | TestRunProfileKind, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + if (this.executing) { + traceError('Test execution already in progress, not starting a new one.'); + return this.executing.promise; + } + + const deferred = createDeferred(); + this.executing = deferred; + + const testCaseNodes: TestItem[] = []; + const testCaseIdsSet = new Set(); + try { + // first fetch all the individual test Items that we necessarily want + includes.forEach((t) => { + const nodes = getTestCaseNodes(t); + testCaseNodes.push(...nodes); + }); + // iterate through testItems nodes and fetch their unittest runID to pass in as argument + testCaseNodes.forEach((node) => { + runInstance.started(node); // do the vscode ui test item start here before runtest + const runId = this.resultResolver.vsIdToRunId.get(node.id); + if (runId) { + testCaseIdsSet.add(runId); + } + }); + const testCaseIds = Array.from(testCaseIdsSet); + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test execution'); + } + await this.executionAdapter.runTests( + this.workspaceUri, + testCaseIds, + profileKind, + runInstance, + executionFactory, + debugLauncher, + interpreter, + project, + ); + deferred.resolve(); + } catch (ex) { + // handle token and telemetry here + sendTelemetryEvent(EventName.UNITTEST_RUN_ALL_FAILED, undefined); + + let cancel = token?.isCancellationRequested + ? Testing.cancelUnittestExecution + : Testing.errorUnittestExecution; + if (this.testProvider === 'pytest') { + cancel = token?.isCancellationRequested ? Testing.cancelPytestExecution : Testing.errorPytestExecution; + } + traceError(`${cancel}\r\n`, ex); + + // Also report on the test view + const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); + const errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + + deferred.reject(ex as Error); + } finally { + this.executing = undefined; + } + + return Promise.resolve(); + } + + public async discoverTests( + testController: TestController, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + ): Promise { + sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); + + // Discovery is expensive. If it is already running, use the existing promise. + if (this.discovering) { + traceError('Test discovery already in progress, not starting a new one.'); + return this.discovering.promise; + } + + const deferred = createDeferred(); + this.discovering = deferred; + + try { + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test discovery'); + } + await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory, token, interpreter); + deferred.resolve(); + } catch (ex) { + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); + + let cancel = token?.isCancellationRequested + ? Testing.cancelUnittestDiscovery + : Testing.errorUnittestDiscovery; + if (this.testProvider === 'pytest') { + cancel = token?.isCancellationRequested ? Testing.cancelPytestDiscovery : Testing.errorPytestDiscovery; + } + + traceError(`${cancel} for workspace: ${this.workspaceUri} \r\n`, ex); + + // Report also on the test view. + const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); + const errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + + return deferred.reject(ex as Error); + } finally { + // Discovery has finished running, we have the data, + // we don't need the deferred promise anymore. + this.discovering = undefined; + } + + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: false }); + return Promise.resolve(); + } + + /** + * Retrieves the current test provider instance. + * + * @returns {TestProvider} The instance of the test provider. + */ + public getTestProvider(): TestProvider { + return this.testProvider; + } +} diff --git a/src/client/testing/types.ts b/src/client/testing/types.ts index 3949e0c502f8..da308ee6998b 100644 --- a/src/client/testing/types.ts +++ b/src/client/testing/types.ts @@ -3,239 +3,15 @@ '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'; +import { Product } from '../common/types'; +import { TestSettingsPropertyNames } from './configuration/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 type TestProvider = 'pytest' | 'unittest'; -export const ITestDataItemResource = Symbol('ITestDataItemResource'); +// **************** +// interfaces -export interface ITestDataItemResource { - getResource(testData: Readonly): Uri; +export const ITestingService = Symbol('ITestingService'); +export interface ITestingService { + getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; } diff --git a/src/client/testing/unittest/helper.ts b/src/client/testing/unittest/helper.ts deleted file mode 100644 index be77ed07a7eb..000000000000 --- a/src/client/testing/unittest/helper.ts +++ /dev/null @@ -1,51 +0,0 @@ -// 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 deleted file mode 100644 index cb81d3de5f23..000000000000 --- a/src/client/testing/unittest/main.ts +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 77f23ee3de54..000000000000 --- a/src/client/testing/unittest/runner.ts +++ /dev/null @@ -1,228 +0,0 @@ -'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 deleted file mode 100644 index 73e27087a01e..000000000000 --- a/src/client/testing/unittest/services/argsService.ts +++ /dev/null @@ -1,84 +0,0 @@ -// 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 deleted file mode 100644 index f09901e37f5f..000000000000 --- a/src/client/testing/unittest/services/discoveryService.ts +++ /dev/null @@ -1,93 +0,0 @@ -// 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 deleted file mode 100644 index 41ad7c9fa28c..000000000000 --- a/src/client/testing/unittest/services/parserService.ts +++ /dev/null @@ -1,125 +0,0 @@ -// 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 deleted file mode 100644 index 9dc23b87e33a..000000000000 --- a/src/client/testing/unittest/socketServer.ts +++ /dev/null @@ -1,125 +0,0 @@ -'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 deleted file mode 100644 index df35f70d2365..000000000000 --- a/src/client/testing/unittest/testConfigurationManager.ts +++ /dev/null @@ -1,34 +0,0 @@ -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/testing/utils.ts b/src/client/testing/utils.ts new file mode 100644 index 000000000000..c1027d4a8dc1 --- /dev/null +++ b/src/client/testing/utils.ts @@ -0,0 +1,49 @@ +import { TestItem, env } from 'vscode'; +import { traceLog } from '../logging'; + +export async function writeTestIdToClipboard(testItem: TestItem): Promise { + if (testItem && typeof testItem.id === 'string') { + if (testItem.id.includes('\\') && testItem.id.indexOf('::') === -1) { + // Convert the id to a module.class.method format as this is a unittest + const moduleClassMethod = idToModuleClassMethod(testItem.id); + if (moduleClassMethod) { + await env.clipboard.writeText(moduleClassMethod); + traceLog('Testing: Copied test id to clipboard, id: ' + moduleClassMethod); + return; + } + } + // Otherwise use the id as is for pytest + await clipboardWriteText(testItem.id); + traceLog('Testing: Copied test id to clipboard, id: ' + testItem.id); + } +} + +export function idToModuleClassMethod(id: string): string | undefined { + // Split by backslash + const parts = id.split('\\'); + if (parts.length === 1) { + // Only one part, likely a parent folder or file + return parts[0]; + } + if (parts.length === 2) { + // Two parts: filePath and className + const [filePath, className] = parts.slice(-2); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}`; + } + // Three or more parts: filePath, className, methodName + const [filePath, className, methodName] = parts.slice(-3); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}.${methodName}`; +} +export function clipboardWriteText(text: string): Thenable { + return env.clipboard.writeText(text); +} diff --git a/src/client/typeFormatters/blockFormatProvider.ts b/src/client/typeFormatters/blockFormatProvider.ts deleted file mode 100644 index 822caa5632e9..000000000000 --- a/src/client/typeFormatters/blockFormatProvider.ts +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 25d677003bb9..000000000000 --- a/src/client/typeFormatters/codeBlockFormatProvider.ts +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 62bf68fd46a3..000000000000 --- a/src/client/typeFormatters/contracts.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 452eb2f38d79..000000000000 --- a/src/client/typeFormatters/dispatcher.ts +++ /dev/null @@ -1,54 +0,0 @@ -// 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 deleted file mode 100644 index f4464b26cf6b..000000000000 --- a/src/client/typeFormatters/onEnterFormatter.ts +++ /dev/null @@ -1,47 +0,0 @@ -// 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/types.ts b/src/client/types.ts new file mode 100644 index 000000000000..8235263e7bab --- /dev/null +++ b/src/client/types.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export type IStartupDurations = Record< + 'totalNonBlockingActivateTime' | 'totalActivateTime' | 'startActivateTime' | 'codeLoadingTime', + number +>; diff --git a/src/client/workspaceSymbols/contracts.ts b/src/client/workspaceSymbols/contracts.ts deleted file mode 100644 index ae447a4e2fbb..000000000000 --- a/src/client/workspaceSymbols/contracts.ts +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index e90db302d26f..000000000000 --- a/src/client/workspaceSymbols/generator.ts +++ /dev/null @@ -1,99 +0,0 @@ -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 deleted file mode 100644 index 256373af7a9c..000000000000 --- a/src/client/workspaceSymbols/main.ts +++ /dev/null @@ -1,152 +0,0 @@ -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 deleted file mode 100644 index 7ec49ba082ce..000000000000 --- a/src/client/workspaceSymbols/parser.ts +++ /dev/null @@ -1,172 +0,0 @@ -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 deleted file mode 100644 index 1719445aa0ae..000000000000 --- a/src/client/workspaceSymbols/provider.ts +++ /dev/null @@ -1,76 +0,0 @@ -'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 deleted file mode 100644 index b65dff477de2..000000000000 --- a/src/datascience-ui/common/cellFactory.ts +++ /dev/null @@ -1,162 +0,0 @@ -// 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 deleted file mode 100644 index b4cc7250b98c..000000000000 --- a/src/datascience-ui/common/index.css +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 3815071f17b3..000000000000 --- a/src/datascience-ui/common/index.ts +++ /dev/null @@ -1,238 +0,0 @@ -// 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 = '"""'; - -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 concatMultilineStringOutput(str: nbformat.MultilineString): string { - return concatMultilineString(str, true); -} -export function concatMultilineStringInput(str: nbformat.MultilineString): string { - return concatMultilineString(str, false); -} - -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 deleted file mode 100644 index 870ae21034f8..000000000000 --- a/src/datascience-ui/common/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 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 deleted file mode 100644 index 10c63a79c8ac..000000000000 --- a/src/datascience-ui/data-explorer/cellFormatter.css +++ /dev/null @@ -1,8 +0,0 @@ -.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 deleted file mode 100644 index 7c29d00a1265..000000000000 --- a/src/datascience-ui/data-explorer/cellFormatter.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// 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.tsx b/src/datascience-ui/data-explorer/emptyRowsView.tsx deleted file mode 100644 index 96eb82385872..000000000000 --- a/src/datascience-ui/data-explorer/emptyRowsView.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// 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 deleted file mode 100644 index d3bfae0a054b..000000000000 --- a/src/datascience-ui/data-explorer/globalJQueryImports.ts +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index b94a027725e1..000000000000 --- a/src/datascience-ui/data-explorer/index.html +++ /dev/null @@ -1,356 +0,0 @@ - - - - - - - React App - - - - - -
- - - diff --git a/src/datascience-ui/data-explorer/index.tsx b/src/datascience-ui/data-explorer/index.tsx deleted file mode 100644 index aa6136295e4e..000000000000 --- a/src/datascience-ui/data-explorer/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index 0616067bf249..000000000000 --- a/src/datascience-ui/data-explorer/mainPanel.css +++ /dev/null @@ -1,13 +0,0 @@ - -.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 deleted file mode 100644 index 7df86002ae67..000000000000 --- a/src/datascience-ui/data-explorer/mainPanel.tsx +++ /dev/null @@ -1,336 +0,0 @@ -// 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 deleted file mode 100644 index 8249651f7029..000000000000 --- a/src/datascience-ui/data-explorer/progressBar.css +++ /dev/null @@ -1,9 +0,0 @@ -.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 deleted file mode 100644 index 97c279b5ca68..000000000000 --- a/src/datascience-ui/data-explorer/progressBar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index b52165279386..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGrid.css +++ /dev/null @@ -1,87 +0,0 @@ -.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 deleted file mode 100644 index 12a67ede2f66..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGrid.tsx +++ /dev/null @@ -1,538 +0,0 @@ -// 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 deleted file mode 100644 index 0d5670650776..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css +++ /dev/null @@ -1,17 +0,0 @@ -.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 deleted file mode 100644 index 12c1a9530cdd..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 963a702ee1a0..000000000000 --- a/src/datascience-ui/data-explorer/testData.ts +++ /dev/null @@ -1,12518 +0,0 @@ -// 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 deleted file mode 100644 index 5f8c85aee905..000000000000 --- a/src/datascience-ui/history-react/index.html +++ /dev/null @@ -1,379 +0,0 @@ - - - - - - - React App - - - - -
- - - - - - - diff --git a/src/datascience-ui/history-react/index.tsx b/src/datascience-ui/history-react/index.tsx deleted file mode 100644 index e50937db6593..000000000000 --- a/src/datascience-ui/history-react/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// 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 deleted file mode 100644 index c5e844c08ff2..000000000000 --- a/src/datascience-ui/history-react/interactiveCell.tsx +++ /dev/null @@ -1,459 +0,0 @@ -// 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 deleted file mode 100644 index abab2ff7e0c1..000000000000 --- a/src/datascience-ui/history-react/interactivePanel.less +++ /dev/null @@ -1,75 +0,0 @@ -/* 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; -} \ No newline at end of file diff --git a/src/datascience-ui/history-react/interactivePanel.tsx b/src/datascience-ui/history-react/interactivePanel.tsx deleted file mode 100644 index a10351ec49f3..000000000000 --- a/src/datascience-ui/history-react/interactivePanel.tsx +++ /dev/null @@ -1,453 +0,0 @@ -// 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 deleted file mode 100644 index 4a8083675c1c..000000000000 --- a/src/datascience-ui/history-react/redux/actions.ts +++ /dev/null @@ -1,113 +0,0 @@ -// 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 deleted file mode 100644 index 92002dfe409b..000000000000 --- a/src/datascience-ui/history-react/redux/mapping.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 5d309b6731c8..000000000000 --- a/src/datascience-ui/history-react/redux/reducers/creation.ts +++ /dev/null @@ -1,203 +0,0 @@ -// 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 deleted file mode 100644 index 83cf3d14104d..000000000000 --- a/src/datascience-ui/history-react/redux/reducers/effects.ts +++ /dev/null @@ -1,173 +0,0 @@ -// 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 deleted file mode 100644 index 1605cc79f8c5..000000000000 --- a/src/datascience-ui/history-react/redux/reducers/execution.ts +++ /dev/null @@ -1,130 +0,0 @@ -// 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 deleted file mode 100644 index 023bf4fc8703..000000000000 --- a/src/datascience-ui/history-react/redux/reducers/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -// 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 -}; diff --git a/src/datascience-ui/history-react/redux/store.ts b/src/datascience-ui/history-react/redux/store.ts deleted file mode 100644 index c9b6551d26e5..000000000000 --- a/src/datascience-ui/history-react/redux/store.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 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 deleted file mode 100644 index 68a2eaa0e89e..000000000000 --- a/src/datascience-ui/interactive-common/buildSettingsCss.ts +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index f46e64e80af9..000000000000 --- a/src/datascience-ui/interactive-common/cellInput.tsx +++ /dev/null @@ -1,209 +0,0 @@ -// 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 { concatMultilineStringInput } 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 = concatMultilineStringInput(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 deleted file mode 100644 index 82449e6ff846..000000000000 --- a/src/datascience-ui/interactive-common/cellOutput.tsx +++ /dev/null @@ -1,595 +0,0 @@ -// 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 { concatMultilineStringInput, concatMultilineStringOutput } 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(concatMultilineStringInput(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 = concatMultilineStringOutput(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 = concatMultilineStringOutput(data as nbformat.MultilineString); - } - - // Fixup latex to make sure it has the requisite $$ around it - if (mimeType === 'text/latex') { - data = fixMarkdown(concatMultilineStringOutput(data as nbformat.MultilineString), 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 deleted file mode 100644 index dd8755e8cec6..000000000000 --- a/src/datascience-ui/interactive-common/code.tsx +++ /dev/null @@ -1,132 +0,0 @@ -// 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 deleted file mode 100644 index 2410ad9eb046..000000000000 --- a/src/datascience-ui/interactive-common/collapseButton.tsx +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index e6387bebac1b..000000000000 --- a/src/datascience-ui/interactive-common/common.css +++ /dev/null @@ -1,563 +0,0 @@ -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: 1.0s; -} - -@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; -} diff --git a/src/datascience-ui/interactive-common/contentPanel.tsx b/src/datascience-ui/interactive-common/contentPanel.tsx deleted file mode 100644 index f8653aa48fa4..000000000000 --- a/src/datascience-ui/interactive-common/contentPanel.tsx +++ /dev/null @@ -1,95 +0,0 @@ -// 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 deleted file mode 100644 index 7eca634b2521..000000000000 --- a/src/datascience-ui/interactive-common/editor.tsx +++ /dev/null @@ -1,277 +0,0 @@ -// 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 deleted file mode 100644 index 538b7ac96cbb..000000000000 --- a/src/datascience-ui/interactive-common/executionCount.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// 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 deleted file mode 100644 index 059ace4951ac..000000000000 --- a/src/datascience-ui/interactive-common/handlers.ts +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index f83b33cbc711..000000000000 --- a/src/datascience-ui/interactive-common/images.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 3d7a6599477d..000000000000 --- a/src/datascience-ui/interactive-common/informationMessages.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index d75dbb4861f7..000000000000 --- a/src/datascience-ui/interactive-common/inputHistory.ts +++ /dev/null @@ -1,91 +0,0 @@ -// 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 deleted file mode 100644 index 035340ca480f..000000000000 --- a/src/datascience-ui/interactive-common/intellisenseProvider.ts +++ /dev/null @@ -1,254 +0,0 @@ -// 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 deleted file mode 100644 index be4fa975691d..000000000000 --- a/src/datascience-ui/interactive-common/jupyterInfo.tsx +++ /dev/null @@ -1,116 +0,0 @@ -// 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'; -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; - 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); - } - - public render() { - const serverTextSize = - getLocString('DataScience.jupyterServer', 'Jupyter Server').length + - this.props.kernel.localizedUri.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) - }; - - return ( - <div className="kernel-status" style={dynamicFont}> - {this.renderTrustMessage()} - <div className="kernel-status-section kernel-status-server" style={serverTextWidth} role="button"> - <div className="kernel-status-text" title={this.props.kernel.localizedUri}> - {getLocString('DataScience.jupyterServer', 'Jupyter Server')}: {this.props.kernel.localizedUri} - </div> - <Image - baseTheme={this.props.baseTheme} - class="image-button-image kernel-status-icon" - image={this.getIcon()} - /> - </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; - } -} diff --git a/src/datascience-ui/interactive-common/mainState.ts b/src/datascience-ui/interactive-common/mainState.ts deleted file mode 100644 index f5327ea54760..000000000000 --- a/src/datascience-ui/interactive-common/mainState.ts +++ /dev/null @@ -1,608 +0,0 @@ -// 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 { concatMultilineStringInput, 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[]; -} - -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; - shouldShowTrustMessage: 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, - shouldShowTrustMessage: 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 - }; -} - -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 concatMultilineStringInput(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 - }; - - // 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' - ? concatMultilineStringInput(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 deleted file mode 100644 index a6a8e083a183..000000000000 --- a/src/datascience-ui/interactive-common/markdown.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// 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 deleted file mode 100644 index f35c06dc7ffd..000000000000 --- a/src/datascience-ui/interactive-common/markdownManipulation.ts +++ /dev/null @@ -1,145 +0,0 @@ -// 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 deleted file mode 100644 index 051fe2ddc0d5..000000000000 --- a/src/datascience-ui/interactive-common/redux/helpers.ts +++ /dev/null @@ -1,150 +0,0 @@ -// 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 deleted file mode 100644 index bfddb6a37a90..000000000000 --- a/src/datascience-ui/interactive-common/redux/postOffice.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index 5ad40578894a..000000000000 --- a/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts +++ /dev/null @@ -1,312 +0,0 @@ -// 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 deleted file mode 100644 index c96590556ea2..000000000000 --- a/src/datascience-ui/interactive-common/redux/reducers/helpers.ts +++ /dev/null @@ -1,120 +0,0 @@ -// 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 (concatMultilineStringInput(arg.prevState.cellVMs[index].cell.data.source) !== concatMultilineStringInput(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 deleted file mode 100644 index 7f8f2a8ad95c..000000000000 --- a/src/datascience-ui/interactive-common/redux/reducers/kernel.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index 532fff8ce1cd..000000000000 --- a/src/datascience-ui/interactive-common/redux/reducers/monaco.ts +++ /dev/null @@ -1,264 +0,0 @@ -// 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 deleted file mode 100644 index 490977928976..000000000000 --- a/src/datascience-ui/interactive-common/redux/reducers/transfer.ts +++ /dev/null @@ -1,330 +0,0 @@ -// 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, IMainState } from '../../mainState'; -import { isSyncingMessage, postActionToExtension } from '../helpers'; -import { Helpers } from './helpers'; -import { - CommonActionType, - CommonReducerArg, - ICellAction, - 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; - } -} diff --git a/src/datascience-ui/interactive-common/redux/reducers/types.ts b/src/datascience-ui/interactive-common/redux/reducers/types.ts deleted file mode 100644 index 9a6b47fc28cc..000000000000 --- a/src/datascience-ui/interactive-common/redux/reducers/types.ts +++ /dev/null @@ -1,260 +0,0 @@ -// 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 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 deleted file mode 100644 index a2b87187f9a8..000000000000 --- a/src/datascience-ui/interactive-common/redux/reducers/variables.ts +++ /dev/null @@ -1,310 +0,0 @@ -// 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 deleted file mode 100644 index 29b76b722420..000000000000 --- a/src/datascience-ui/interactive-common/redux/store.ts +++ /dev/null @@ -1,448 +0,0 @@ -// 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 { 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, - shouldShowTrustMessage: false - }; - } -} - -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 - 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(); - }); - }; - - // 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) { - // 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 deleted file mode 100644 index a1e353ed715f..000000000000 --- a/src/datascience-ui/interactive-common/tokenizer.ts +++ /dev/null @@ -1,81 +0,0 @@ -// 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 deleted file mode 100644 index d9ac734a6e72..000000000000 --- a/src/datascience-ui/interactive-common/transforms.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* 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 deleted file mode 100644 index 9300356204c1..000000000000 --- a/src/datascience-ui/interactive-common/trimmedOutputLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index c97a97e6c6cb..000000000000 --- a/src/datascience-ui/interactive-common/trustMessage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index 11e62d3b8a70..000000000000 --- a/src/datascience-ui/interactive-common/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 5a47b522b165..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorer.css +++ /dev/null @@ -1,24 +0,0 @@ -.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 deleted file mode 100644 index 7d4ea054d751..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorer.tsx +++ /dev/null @@ -1,460 +0,0 @@ -// 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 deleted file mode 100644 index 8ad219b7558c..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.css +++ /dev/null @@ -1,4 +0,0 @@ -.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 deleted file mode 100644 index cda13175141f..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// 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 deleted file mode 100644 index ffdc679ccacb..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerCellFormatter.css +++ /dev/null @@ -1,32 +0,0 @@ -.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 deleted file mode 100644 index eea7b1db1b66..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerCellFormatter.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index 23fe2407049e..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerEmptyRows.css +++ /dev/null @@ -1,4 +0,0 @@ -#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 deleted file mode 100644 index 1668e1d35c99..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerEmptyRows.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// 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 deleted file mode 100644 index eb261d0ba265..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerGrid.less +++ /dev/null @@ -1,127 +0,0 @@ -/* 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 deleted file mode 100644 index 69840df5d7bb..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerHeaderCellFormatter.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index c346a7ef3ead..000000000000 --- a/src/datascience-ui/interactive-common/variableExplorerRowRenderer.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index dbd577c54ee7..000000000000 --- a/src/datascience-ui/interactive-common/variablePanel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index 1162960b7ba5..000000000000 --- a/src/datascience-ui/ipywidgets/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# 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 deleted file mode 100644 index ef5fed6a19d5..000000000000 --- a/src/datascience-ui/ipywidgets/container.tsx +++ /dev/null @@ -1,280 +0,0 @@ -// 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 deleted file mode 100644 index a6d3ce4e1207..000000000000 --- a/src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 6d243f31da68..000000000000 --- a/src/datascience-ui/ipywidgets/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index c3efa52b5724..000000000000 --- a/src/datascience-ui/ipywidgets/kernel.ts +++ /dev/null @@ -1,514 +0,0 @@ -// 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 deleted file mode 100644 index 88d4b5a07750..000000000000 --- a/src/datascience-ui/ipywidgets/manager.ts +++ /dev/null @@ -1,216 +0,0 @@ -// 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 deleted file mode 100644 index bf5c7755ccec..000000000000 --- a/src/datascience-ui/ipywidgets/requirejsRegistry.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 528eb0035d69..000000000000 --- a/src/datascience-ui/ipywidgets/types.ts +++ /dev/null @@ -1,85 +0,0 @@ -// 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 deleted file mode 100644 index f5513aafecc9..000000000000 --- a/src/datascience-ui/native-editor/addCellLine.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index dbc0b671d27a..000000000000 --- a/src/datascience-ui/native-editor/index.html +++ /dev/null @@ -1,392 +0,0 @@ -<!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 deleted file mode 100644 index 22c592903b56..000000000000 --- a/src/datascience-ui/native-editor/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index 625a53c98990..000000000000 --- a/src/datascience-ui/native-editor/nativeCell.tsx +++ /dev/null @@ -1,904 +0,0 @@ -// 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 { concatMultilineStringInput } 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 || concatMultilineStringInput(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} - > - <Image - baseTheme={this.props.baseTheme} - class="image-button-image" - image={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 deleted file mode 100644 index 8b32bd39e309..000000000000 --- a/src/datascience-ui/native-editor/nativeEditor.less +++ /dev/null @@ -1,494 +0,0 @@ -/* 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 deleted file mode 100644 index 35f4eede3d29..000000000000 --- a/src/datascience-ui/native-editor/nativeEditor.tsx +++ /dev/null @@ -1,397 +0,0 @@ -// 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 - shouldShowTrustMessage={this.props.shouldShowTrustMessage} - 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 ((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 deleted file mode 100644 index b1c83818a5d4..000000000000 --- a/src/datascience-ui/native-editor/redux/actions.ts +++ /dev/null @@ -1,150 +0,0 @@ -// 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 deleted file mode 100644 index 354cbe665814..000000000000 --- a/src/datascience-ui/native-editor/redux/mapping.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index a569b8aa0edb..000000000000 --- a/src/datascience-ui/native-editor/redux/reducers/creation.ts +++ /dev/null @@ -1,493 +0,0 @@ -// 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 - }; - - 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 - }; - - // 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!, - shouldShowTrustMessage: arg.payload.data.shouldShowTrustMessage! - }; - } - - 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 deleted file mode 100644 index 982e4776d290..000000000000 --- a/src/datascience-ui/native-editor/redux/reducers/effects.ts +++ /dev/null @@ -1,247 +0,0 @@ -// 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 deleted file mode 100644 index 6666e4a36869..000000000000 --- a/src/datascience-ui/native-editor/redux/reducers/execution.ts +++ /dev/null @@ -1,357 +0,0 @@ -// 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 { concatMultilineStringInput } 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' && concatMultilineStringInput(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 deleted file mode 100644 index 86bb80258c7a..000000000000 --- a/src/datascience-ui/native-editor/redux/reducers/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -// 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, - [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 deleted file mode 100644 index bacac77d8398..000000000000 --- a/src/datascience-ui/native-editor/redux/reducers/movement.ts +++ /dev/null @@ -1,91 +0,0 @@ -// 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 deleted file mode 100644 index 53f228f59411..000000000000 --- a/src/datascience-ui/native-editor/redux/store.ts +++ /dev/null @@ -1,12 +0,0 @@ -// 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 deleted file mode 100644 index 97c3d76cb5b3..000000000000 --- a/src/datascience-ui/native-editor/toolbar.tsx +++ /dev/null @@ -1,278 +0,0 @@ -// 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 { 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; -}; -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; - shouldShowTrustMessage: 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={this.props.shouldShowTrustMessage} - isNotebookTrusted={this.props.isNotebookTrusted} - launchNotebookTrustPrompt={launchNotebookTrustPrompt} - /> - </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 deleted file mode 100644 index 9dd3ffa71749..000000000000 --- a/src/datascience-ui/plot/index.html +++ /dev/null @@ -1,356 +0,0 @@ -<!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 deleted file mode 100644 index aa6136295e4e..000000000000 --- a/src/datascience-ui/plot/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index aa507ba5a6b9..000000000000 --- a/src/datascience-ui/plot/mainPanel.css +++ /dev/null @@ -1,13 +0,0 @@ - -.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 deleted file mode 100644 index a706aabe6f61..000000000000 --- a/src/datascience-ui/plot/mainPanel.tsx +++ /dev/null @@ -1,428 +0,0 @@ -// 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 deleted file mode 100644 index 64bfd3e54221..000000000000 --- a/src/datascience-ui/plot/testSvg.ts +++ /dev/null @@ -1,571 +0,0 @@ -// 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 deleted file mode 100644 index 82df52ea1834..000000000000 --- a/src/datascience-ui/plot/toolbar.css +++ /dev/null @@ -1,7 +0,0 @@ -#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 deleted file mode 100644 index 692fe4f68ed6..000000000000 --- a/src/datascience-ui/plot/toolbar.tsx +++ /dev/null @@ -1,99 +0,0 @@ -// 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 deleted file mode 100644 index 8e46dc12f41d..000000000000 --- a/src/datascience-ui/react-common/arePathsSame.ts +++ /dev/null @@ -1,17 +0,0 @@ -// 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 deleted file mode 100644 index e11cb502714a..000000000000 --- a/src/datascience-ui/react-common/button.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index abfde40dede6..000000000000 --- a/src/datascience-ui/react-common/codicon/codicon-animations.css +++ /dev/null @@ -1,14 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 deleted file mode 100644 index 950493dd6bed..000000000000 --- a/src/datascience-ui/react-common/codicon/codicon-modifications.css +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 deleted file mode 100644 index 70e60afd42af..000000000000 --- a/src/datascience-ui/react-common/codicon/codicon.css +++ /dev/null @@ -1,429 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 deleted file mode 100644 index 65ca2f1662aa..000000000000 --- a/src/datascience-ui/react-common/codicon/codicon.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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 deleted file mode 100644 index b4066b7d08c4..000000000000 Binary files a/src/datascience-ui/react-common/codicon/codicon.ttf and /dev/null differ diff --git a/src/datascience-ui/react-common/constants.ts b/src/datascience-ui/react-common/constants.ts deleted file mode 100644 index c7bd757316d7..000000000000 --- a/src/datascience-ui/react-common/constants.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index a37bc55cc147..000000000000 --- a/src/datascience-ui/react-common/errorBoundary.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// 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 deleted file mode 100644 index ab2fed369556..000000000000 --- a/src/datascience-ui/react-common/event.ts +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index 3f9ad5ce827d..000000000000 --- a/src/datascience-ui/react-common/flyout.css +++ /dev/null @@ -1,20 +0,0 @@ - -.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 deleted file mode 100644 index 3e3f634eddfc..000000000000 --- a/src/datascience-ui/react-common/flyout.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// 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 deleted file mode 100644 index 6248f8f789d0..000000000000 --- a/src/datascience-ui/react-common/image.tsx +++ /dev/null @@ -1,249 +0,0 @@ -// 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 -} - -// 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') - } -}; - -interface IImageProps { - baseTheme: string; - image: ImageName; - class: 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} />; - } -} diff --git a/src/datascience-ui/react-common/imageButton.css b/src/datascience-ui/react-common/imageButton.css deleted file mode 100644 index 1b47cd8b6d8e..000000000000 --- a/src/datascience-ui/react-common/imageButton.css +++ /dev/null @@ -1,50 +0,0 @@ -: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 deleted file mode 100644 index 6105b49ec693..000000000000 --- a/src/datascience-ui/react-common/imageButton.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index 6cf0d7996f55..000000000000 --- a/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index c4df2f91fee2..000000000000 --- a/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index f8811f5ea310..000000000000 --- a/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_dark.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index 2c2015b6cb27..000000000000 --- a/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_light.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index ffddd4cbdb95..000000000000 --- a/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index 11e1f39ad968..000000000000 --- a/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index c8d97a3e23be..000000000000 --- a/src/datascience-ui/react-common/images/Copy/copy.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index b5d2606adb65..000000000000 --- a/src/datascience-ui/react-common/images/Copy/copy_inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 909517562dfb..000000000000 --- a/src/datascience-ui/react-common/images/Delete/delete_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 708df4baba32..000000000000 --- a/src/datascience-ui/react-common/images/Delete/delete_light.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index d3015672bd49..000000000000 --- a/src/datascience-ui/react-common/images/Down/down-inverse.svg +++ /dev/null @@ -1,32 +0,0 @@ -<?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 deleted file mode 100644 index 30d5faa11be2..000000000000 --- a/src/datascience-ui/react-common/images/Down/down.svg +++ /dev/null @@ -1,32 +0,0 @@ -<?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 deleted file mode 100644 index d82b92292bee..000000000000 --- a/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode.svg +++ /dev/null @@ -1,5 +0,0 @@ -<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 deleted file mode 100644 index 8db4b6139c80..000000000000 --- a/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode_dark.svg +++ /dev/null @@ -1,5 +0,0 @@ -<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 deleted file mode 100644 index a68ca2942cb7..000000000000 --- a/src/datascience-ui/react-common/images/ExportToPython/export_to_python_dark.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index 873383aaeb21..000000000000 --- a/src/datascience-ui/react-common/images/ExportToPython/export_to_python_light.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index 135fc3c97683..000000000000 --- a/src/datascience-ui/react-common/images/GatherCode/gather_dark.svg +++ /dev/null @@ -1,12 +0,0 @@ -<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 deleted file mode 100644 index 72ec63e06b65..000000000000 --- a/src/datascience-ui/react-common/images/GatherCode/gather_light.svg +++ /dev/null @@ -1,13 +0,0 @@ -<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 deleted file mode 100644 index d4bca23d4a0e..000000000000 --- a/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index b54d042ca46d..000000000000 --- a/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 6b36517f6efa..000000000000 --- a/src/datascience-ui/react-common/images/InsertAbove/above-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index cc0ee1bbd824..000000000000 --- a/src/datascience-ui/react-common/images/InsertAbove/above.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 6b36517f6efa..000000000000 --- a/src/datascience-ui/react-common/images/InsertBelow/below-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index cc0ee1bbd824..000000000000 --- a/src/datascience-ui/react-common/images/InsertBelow/below.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 391089006ec5..000000000000 --- a/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 391089006ec5..000000000000 --- a/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index f44818031c24..000000000000 --- a/src/datascience-ui/react-common/images/JupyterServerConnected/connected-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index f44818031c24..000000000000 --- a/src/datascience-ui/react-common/images/JupyterServerConnected/connected-hc.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 28987e1c1723..000000000000 --- a/src/datascience-ui/react-common/images/JupyterServerConnected/connected-light.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 02a598dc9878..000000000000 --- a/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 02a598dc9878..000000000000 --- a/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-hc.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 107b8e22f7c3..000000000000 --- a/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-light.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index d1c3e8dbd956..000000000000 --- a/src/datascience-ui/react-common/images/Next/next-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 465903659681..000000000000 --- a/src/datascience-ui/react-common/images/Next/next.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index d50b0367205a..000000000000 --- a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 93a0af00cad6..000000000000 --- a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 8ac26cda0805..000000000000 --- a/src/datascience-ui/react-common/images/OpenPlot/plot_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 67600ef32a6e..000000000000 --- a/src/datascience-ui/react-common/images/OpenPlot/plot_light.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 25f33c9e5b4b..000000000000 --- a/src/datascience-ui/react-common/images/Pan/pan.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 75e2f52f6c4f..000000000000 --- a/src/datascience-ui/react-common/images/Pan/pan_inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 906f8244eb8b..000000000000 --- a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ -<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 deleted file mode 100644 index 041b6e015305..000000000000 --- a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode_dark.svg +++ /dev/null @@ -1 +0,0 @@ -<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 deleted file mode 100644 index e979568ea576..000000000000 --- a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ -<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 deleted file mode 100644 index a72e56c2dc70..000000000000 --- a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode_dark.svg +++ /dev/null @@ -1 +0,0 @@ -<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 deleted file mode 100644 index d779fba43c47..000000000000 --- a/src/datascience-ui/react-common/images/Prev/previous-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 730afafcfd32..000000000000 --- a/src/datascience-ui/react-common/images/Prev/previous.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 3e537188666c..000000000000 --- a/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 5c1c7c98bc5c..000000000000 --- a/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 4ce884e15bbe..000000000000 --- a/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 720bc53df88c..000000000000 --- a/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 345289f1e577..000000000000 --- a/src/datascience-ui/react-common/images/Run/run-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index ef2819326598..000000000000 --- a/src/datascience-ui/react-common/images/Run/run-light.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index d8f93fb68a32..000000000000 --- a/src/datascience-ui/react-common/images/RunAbove/runabove-inverse.svg +++ /dev/null @@ -1,12 +0,0 @@ -<?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 deleted file mode 100644 index 8b5ccb9ee047..000000000000 --- a/src/datascience-ui/react-common/images/RunAbove/runabove.svg +++ /dev/null @@ -1,12 +0,0 @@ -<?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 deleted file mode 100644 index e9497b351ed4..000000000000 --- a/src/datascience-ui/react-common/images/RunAll/run_all_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 36af4345aa03..000000000000 --- a/src/datascience-ui/react-common/images/RunAll/run_all_light.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 1b820cd29fb9..000000000000 --- a/src/datascience-ui/react-common/images/RunBelow/runbelow-inverse.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?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 deleted file mode 100644 index 38730888f13a..000000000000 --- a/src/datascience-ui/react-common/images/RunBelow/runbelow.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?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 deleted file mode 100644 index 5193f5d4902e..000000000000 --- a/src/datascience-ui/react-common/images/RunByLine/runbyline_dark.svg +++ /dev/null @@ -1,15 +0,0 @@ -<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 deleted file mode 100644 index 2a18ee6ac53a..000000000000 --- a/src/datascience-ui/react-common/images/RunByLine/runbyline_light.svg +++ /dev/null @@ -1,15 +0,0 @@ -<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 deleted file mode 100644 index 2b461e5c7568..000000000000 --- a/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 865c8fc584cb..000000000000 --- a/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 87819244977e..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/Interactive-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 6731d50fee39..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/Interactive.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 12b217d48fb8..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/Notebook-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 5e058afb30eb..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/Notebook.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index e3d0f3e933e7..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/OpenFolder-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 309858c9f469..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/OpenFolder.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index e9b909eab46b..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/Python-color.svg +++ /dev/null @@ -1,14 +0,0 @@ -<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 deleted file mode 100644 index 73708a57db29..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/Python-inverse.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index 9a4d621e91c0..000000000000 --- a/src/datascience-ui/react-common/images/StartPage/Python.svg +++ /dev/null @@ -1,4 +0,0 @@ -<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 deleted file mode 100644 index 3b93100ffac3..000000000000 --- a/src/datascience-ui/react-common/images/SwitchToCode/switchtocode-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index bc21caf9f4b6..000000000000 --- a/src/datascience-ui/react-common/images/SwitchToCode/switchtocode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 388652f13b4c..000000000000 --- a/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 34347fe785d1..000000000000 --- a/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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/Undo/Undo_16x_vscode.svg b/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode.svg deleted file mode 100644 index 670fc2eb3404..000000000000 --- a/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 4db7708d1b13..000000000000 --- a/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 72f710713430..000000000000 --- a/src/datascience-ui/react-common/images/Up/up-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index ff5354d2977a..000000000000 --- a/src/datascience-ui/react-common/images/Up/up.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 4deff429c333..000000000000 --- a/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 6fdbb1675a37..000000000000 --- a/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_light.svg +++ /dev/null @@ -1,3 +0,0 @@ -<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 deleted file mode 100644 index 213e6224adb6..000000000000 --- a/src/datascience-ui/react-common/images/Zoom/zoom.svg +++ /dev/null @@ -1,11 +0,0 @@ -<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 deleted file mode 100644 index 8cecde9b3b9d..000000000000 --- a/src/datascience-ui/react-common/images/Zoom/zoom_inverse.svg +++ /dev/null @@ -1,11 +0,0 @@ -<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 deleted file mode 100644 index 009bb8b89e0e..000000000000 --- a/src/datascience-ui/react-common/images/ZoomOut/zoomout.svg +++ /dev/null @@ -1,11 +0,0 @@ -<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 deleted file mode 100644 index a2e887d89c74..000000000000 --- a/src/datascience-ui/react-common/images/ZoomOut/zoomout_inverse.svg +++ /dev/null @@ -1,11 +0,0 @@ -<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 deleted file mode 100644 index dec1f5fa6946..000000000000 --- a/src/datascience-ui/react-common/locReactSide.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index b062fac823ff..000000000000 --- a/src/datascience-ui/react-common/logger.ts +++ /dev/null @@ -1,15 +0,0 @@ -// 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 deleted file mode 100644 index eb52202d2c7e..000000000000 --- a/src/datascience-ui/react-common/monacoEditor.css +++ /dev/null @@ -1,123 +0,0 @@ -.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 deleted file mode 100644 index cc1136d77df5..000000000000 --- a/src/datascience-ui/react-common/monacoEditor.tsx +++ /dev/null @@ -1,1049 +0,0 @@ -// 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 deleted file mode 100644 index 0258d9c86fea..000000000000 --- a/src/datascience-ui/react-common/monacoHelpers.ts +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index 0d0c63260d67..000000000000 --- a/src/datascience-ui/react-common/postOffice.ts +++ /dev/null @@ -1,115 +0,0 @@ -// 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 { WebPanelMessage } 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 WebPanelMessage; - 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 deleted file mode 100644 index c5802e7ee863..000000000000 --- a/src/datascience-ui/react-common/progress.css +++ /dev/null @@ -1,70 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 deleted file mode 100644 index 4bbf7981f108..000000000000 --- a/src/datascience-ui/react-common/progress.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index b651517aff8a..000000000000 --- a/src/datascience-ui/react-common/reduxUtils.ts +++ /dev/null @@ -1,94 +0,0 @@ -// 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 deleted file mode 100644 index 9c67995b7c51..000000000000 --- a/src/datascience-ui/react-common/relativeImage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index 04d8b78f26b3..000000000000 --- a/src/datascience-ui/react-common/settingsReactSide.ts +++ /dev/null @@ -1,121 +0,0 @@ -// 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 deleted file mode 100644 index 4ff54f1786e5..000000000000 --- a/src/datascience-ui/react-common/styleInjector.tsx +++ /dev/null @@ -1,120 +0,0 @@ -// 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 deleted file mode 100644 index 6c7d9e34ba39..000000000000 --- a/src/datascience-ui/react-common/svgList.css +++ /dev/null @@ -1,36 +0,0 @@ -.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 deleted file mode 100644 index b3e75028d71c..000000000000 --- a/src/datascience-ui/react-common/svgList.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// 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.tsx b/src/datascience-ui/react-common/svgViewer.tsx deleted file mode 100644 index e477eca355fa..000000000000 --- a/src/datascience-ui/react-common/svgViewer.tsx +++ /dev/null @@ -1,112 +0,0 @@ -// 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 deleted file mode 100644 index 8e75f75b5da7..000000000000 --- a/src/datascience-ui/react-common/textMeasure.ts +++ /dev/null @@ -1,25 +0,0 @@ -// 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 deleted file mode 100644 index b217a7334f22..000000000000 --- a/src/datascience-ui/react-common/themeDetector.ts +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index bc14f44742b4..000000000000 --- a/src/datascience-ui/renderers/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 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 deleted file mode 100644 index 3135551d958c..000000000000 --- a/src/datascience-ui/renderers/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -// 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 '../../client/common/extensions'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; -import { handleLinkClick } from '../interactive-common/handlers'; -import { JupyterNotebookRenderer } from './constants'; -import { CellOutput } from './render'; - -const notebookApi = acquireNotebookRendererApi(JupyterNotebookRenderer); - -notebookApi.onDidCreateOutput(({ element }) => renderOutput(element.querySelector('script')!)); - -/** - * Called from renderer to render output. - * This will be exposed as a public method on window for renderer to render output. - */ -function renderOutput(tag: HTMLScriptElement) { - let container: HTMLElement; - const mimeType = tag.dataset.mimeType as string; - try { - const output = JSON.parse(tag.innerHTML) as nbformat.IExecuteResult | nbformat.IDisplayData; - // tslint:disable-next-line: no-console - console.log(`Rendering mimeType ${mimeType}`, output); - - // Create an element to render in, or reuse a previous element. - const maybeOldContainer = tag.previousElementSibling; - if (maybeOldContainer instanceof HTMLDivElement && maybeOldContainer.dataset.renderer) { - container = maybeOldContainer; - // tslint:disable-next-line: no-inner-html - container.innerHTML = ''; - } else { - container = document.createElement('div'); - tag.parentNode?.insertBefore(container, tag.nextSibling); - } - - ReactDOM.render(React.createElement(CellOutput, { mimeType, output }, null), container); - } catch (ex) { - // tslint:disable-next-line: no-console - console.error(`Failed to render mime type ${mimeType}`, ex); - } -} - -/** - * Possible the pre-render scripts load late, after we have attempted to render output from notebook. - * At this point look through all such scripts and render the output. - */ -function renderOnLoad() { - document - .querySelectorAll<HTMLScriptElement>('script[type="application/vscode-jupyter+json"]') - .forEach(renderOutput); -} - -// tslint:disable-next-line: no-any -function postToExtension(type: string, payload: any) { - notebookApi.postMessage({ type, payload }); -} -function linkHandler(href: string) { - if (href.startsWith('data:image/png')) { - postToExtension(InteractiveWindowMessages.SavePng, href); - } else { - postToExtension(InteractiveWindowMessages.OpenLink, href); - } -} - -// tslint:disable-next-line: no-any -function initialize() { - document.addEventListener('click', (e) => handleLinkClick(e, linkHandler), true); - // Possible this (pre-render script loaded after notebook attempted to render something). - // At this point we need to go and render the existing output. - renderOnLoad(); -} - -// tslint:disable-next-line: no-console -console.log('Pre-Render scripts loaded'); -initialize(); diff --git a/src/datascience-ui/renderers/render.tsx b/src/datascience-ui/renderers/render.tsx deleted file mode 100644 index 4ad9bb17888b..000000000000 --- a/src/datascience-ui/renderers/render.tsx +++ /dev/null @@ -1,96 +0,0 @@ -// 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 { concatMultilineStringOutput } 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(concatMultilineStringOutput(data as nbformat.MultilineString), true); - return this.renderOutput(data); - } -} - -function isAltairPlot(mimeType: string) { - return mimeType.includes('application/vnd.vega'); -} diff --git a/src/datascience-ui/renderers/webviewApi.d.ts b/src/datascience-ui/renderers/webviewApi.d.ts deleted file mode 100644 index 3d595b8c00f4..000000000000 --- a/src/datascience-ui/renderers/webviewApi.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -interface Disposable { - dispose(): void; -} - -export interface Event<T> { - (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable; -} - -declare global { - export interface INotebookRendererApi<T> { - setState(value: T): void; - getState(): T | undefined; - - /** - * Sends a message to the renderer extension code. Can be received in - * the `onDidReceiveMessage` event in `NotebookCommunication`. - */ - postMessage(msg: unknown): void; - - /** - * Fired before an output is destroyed, with its output ID, or undefined if - * all cells are about to unmount. - */ - onWillDestroyOutput: Event<{ outputId: string } | undefined>; - - /** - * Fired when an output is rendered. The `outputId` provided is the same - * as the one given in {@see NotebookOutputRenderer.render} - * and {@see onWillDestroyOutput}. - */ - onDidCreateOutput: Event<{ element: HTMLElement; outputId: string }>; - - /** - * Called when the renderer uses `postMessage` on the NotebookCommunication - * instance for this renderer. - */ - onDidReceiveMessage: Event<any>; - } - - function acquireNotebookRendererApi<T = any>(rendererType: string): INotebookRendererApi<T>; -} diff --git a/src/datascience-ui/startPage/index.html b/src/datascience-ui/startPage/index.html deleted file mode 100644 index b405e61a8e80..000000000000 --- a/src/datascience-ui/startPage/index.html +++ /dev/null @@ -1,356 +0,0 @@ -<!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 deleted file mode 100644 index 1b9828708914..000000000000 --- a/src/datascience-ui/startPage/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index 035610dcebf8..000000000000 --- a/src/datascience-ui/startPage/startPage.css +++ /dev/null @@ -1,93 +0,0 @@ -.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 deleted file mode 100644 index 3794c22919fe..000000000000 --- a/src/datascience-ui/startPage/startPage.tsx +++ /dev/null @@ -1,293 +0,0 @@ -// 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 deleted file mode 100644 index 9095dd4beb82..000000000000 --- a/src/ipywidgets/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.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 deleted file mode 100644 index a726efb9a208..000000000000 --- a/src/ipywidgets/.vscode/settings.json +++ /dev/null @@ -1,41 +0,0 @@ -// 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 - } -} diff --git a/src/ipywidgets/README.md b/src/ipywidgets/README.md deleted file mode 100644 index 35736be4092b..000000000000 --- a/src/ipywidgets/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# 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 deleted file mode 100644 index 7aef5889121e..000000000000 --- a/src/ipywidgets/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index acbfb2be97bc..000000000000 --- a/src/ipywidgets/scripts/clean.js +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index 19bf24824656..000000000000 --- a/src/ipywidgets/scripts/copyfiles.js +++ /dev/null @@ -1,13 +0,0 @@ -// 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 deleted file mode 100644 index 5c1bf0fed5cc..000000000000 --- a/src/ipywidgets/src/documentContext.ts +++ /dev/null @@ -1,95 +0,0 @@ -// 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 deleted file mode 100644 index ecafca9e9810..000000000000 --- a/src/ipywidgets/src/embed.ts +++ /dev/null @@ -1,112 +0,0 @@ -// 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 deleted file mode 100644 index f428e7dbe8f8..000000000000 --- a/src/ipywidgets/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index 8fcc765f08c2..000000000000 --- a/src/ipywidgets/src/libembed.ts +++ /dev/null @@ -1,91 +0,0 @@ -// 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 deleted file mode 100644 index d529bb4b00ee..000000000000 --- a/src/ipywidgets/src/manager.ts +++ /dev/null @@ -1,153 +0,0 @@ -// 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. - } - - 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 deleted file mode 100644 index ea7063145c37..000000000000 --- a/src/ipywidgets/src/signal.ts +++ /dev/null @@ -1,26 +0,0 @@ -// 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 deleted file mode 100644 index 112e7c5b40cf..000000000000 --- a/src/ipywidgets/src/widgetLoader.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 1186770feb07..000000000000 --- a/src/ipywidgets/src/widgets.css +++ /dev/null @@ -1,9 +0,0 @@ -/* 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 deleted file mode 100644 index bc5348c1c7d8..000000000000 --- a/src/ipywidgets/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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 deleted file mode 100644 index 70343d7fc002..000000000000 --- a/src/ipywidgets/types/index.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 1ea978810180..000000000000 --- a/src/ipywidgets/types/require.js.d.ts +++ /dev/null @@ -1,414 +0,0 @@ -// 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 deleted file mode 100644 index 420925a1fcf6..000000000000 --- a/src/ipywidgets/webpack.config.js +++ /dev/null @@ -1,164 +0,0 @@ -// 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/test/.vscode/.ropeproject/config.py b/src/test/.vscode/.ropeproject/config.py new file mode 100644 index 000000000000..dee2d1ae9a6b --- /dev/null +++ b/src/test/.vscode/.ropeproject/config.py @@ -0,0 +1,114 @@ +# The default ``config.py`` +# flake8: noqa + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git', '.tox'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + # prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + # prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + # prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + # If `True`, rope will insert new module imports as + # `from <package> import <module>` by default. + prefs['prefer_module_from_imports'] = False + + # If `True`, rope will transform a comma list of imports into + # multiple separate import statements when organizing + # imports. + prefs['split_imports'] = False + + # If `True`, rope will remove all top-level import statements and + # reinsert them at the top of the module when making changes. + prefs['pull_imports_to_top'] = True + + # If `True`, rope will sort imports alphabetically by module name instead + # of alphabetically by import statement, with from imports after normal + # imports. + prefs['sort_imports_alphabetically'] = False + + # Location of implementation of + # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general + # case, you don't have to change this value, unless you're an rope expert. + # Change this value to inject you own implementations of interfaces + # listed in module rope.base.oi.type_hinting.providers.interfaces + # For example, you can add you own providers for Django Models, or disable + # the search type-hinting in a class hierarchy, etc. + prefs['type_hinting_factory'] = ( + 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/src/test/.vscode/.ropeproject/objectdb b/src/test/.vscode/.ropeproject/objectdb new file mode 100644 index 000000000000..0a47446c0ad2 Binary files /dev/null and b/src/test/.vscode/.ropeproject/objectdb differ diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index f997865a5301..cd2b4152591d 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -1,19 +1,8 @@ { "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.testing.unittestArgs": ["-s=./tests", "-p=test_*.py", "-v", "-s", ".", "-p", "*test*.py"], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.linting.pycodestyleEnabled": false, @@ -22,11 +11,8 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.linting.banditEnabled": false, - "python.formatting.provider": "yapf", - "python.linting.pylintUseMinimalCheckers": false, - "python.pythonPath": "python", - // 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" + // Don't set this to `Pylance`, for CI we want to use the LS that ships with the extension. + "python.languageServer": "Jedi", + "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe", + "python.defaultInterpreterPath": "python" } diff --git a/src/test/.vscode/tags b/src/test/.vscode/tags deleted file mode 100644 index e4dc3f827c89..000000000000 --- a/src/test/.vscode/tags +++ /dev/null @@ -1,721 +0,0 @@ -!_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 deleted file mode 100644 index 97cba28f3e03..000000000000 --- a/src/test/activation/aaTesting.unit.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 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 index c4fc941ed17b..6ee2572214b8 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -5,47 +5,33 @@ import { assert, expect } from 'chai'; import * as sinon from 'sinon'; -import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; -import { TextDocument, Uri } from 'vscode'; +import { TextDocument, Uri, WorkspaceFolder } 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 { IDisposable, IInterpreterPathService } from '../../client/common/types'; +import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; 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(); } @@ -54,60 +40,149 @@ suite('Activation Manager', () => { 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>(); + interpreterPathService + .setup((i) => i.copyOldInterpreterStorageValuesToNew(typemoq.It.isAny())) + .returns(() => Promise.resolve()); 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); + when(workspaceService.isTrusted).thenReturn(true); + when(workspaceService.isVirtualWorkspace).thenReturn(false); 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('If running in a virtual workspace, do not activate services that do not support it', async () => { + when(workspaceService.isVirtualWorkspace).thenReturn(true); + const resource = Uri.parse('two'); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + + 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()); + + managerTest = new ExtensionActivationManagerTest( + [], + [], + documentManager.object, + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + interpreterPathService.object, + ); + await managerTest.activateWorkspace(resource); + + autoSelection.verifyAll(); + appDiagnostics.verifyAll(); + }); + + test('If running in a untrusted workspace, do not activate services that do not support it', async () => { + when(workspaceService.isTrusted).thenReturn(false); + const resource = Uri.parse('two'); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + managerTest = new ExtensionActivationManagerTest( + [], + [], + documentManager.object, + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + interpreterPathService.object, + ); + await managerTest.activateWorkspace(resource); + + appDiagnostics.verifyAll(); + }); + + test('Otherwise activate all services filtering to the current resource', async () => { + const resource = Uri.parse('two'); + + 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()); + + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + + await managerTest.activateWorkspace(resource); + + autoSelection.verifyAll(); + appDiagnostics.verifyAll(); + }); + 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); + when(workspaceService.workspaceFolders).thenReturn([ + (1 as unknown) as WorkspaceFolder, + (2 as unknown) as WorkspaceFolder, + ]); const eventDef = () => disposable2.object; documentManager .setup((d) => d.onDidOpenTextDocument) @@ -117,7 +192,6 @@ suite('Activation Manager', () => { await managerTest.initialize(); verify(workspaceService.workspaceFolders).once(); - verify(workspaceService.hasWorkspaceFolders).once(); verify(workspaceService.onDidChangeWorkspaceFolders).once(); documentManager.verifyAll(); @@ -134,8 +208,10 @@ suite('Activation Manager', () => { 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); + when(workspaceService.workspaceFolders).thenReturn([ + (1 as unknown) as WorkspaceFolder, + (2 as unknown) as WorkspaceFolder, + ]); const eventDef = () => disposable2.object; documentManager .setup((d) => d.onDidOpenTextDocument) @@ -147,18 +223,15 @@ suite('Activation Manager', () => { 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()); @@ -171,6 +244,7 @@ suite('Activation Manager', () => { const disposable1 = typemoq.Mock.ofType<IDisposable>(); const disposable2 = typemoq.Mock.ofType<IDisposable>(); let fileOpenedHandler!: (e: TextDocument) => Promise<void>; + // eslint-disable-next-line @typescript-eslint/ban-types let workspaceFoldersChangedHandler!: Function; const documentUri = Uri.file('a'); const document = typemoq.Mock.ofType<TextDocument>(); @@ -183,7 +257,9 @@ suite('Activation Manager', () => { }); documentManager .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((cb) => (fileOpenedHandler = cb)) + .callback((cb) => { + fileOpenedHandler = cb; + }) .returns(() => disposable2.object) .verifiable(typemoq.Times.once()); @@ -192,13 +268,9 @@ suite('Activation Manager', () => { 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()) @@ -222,63 +294,12 @@ suite('Activation Manager', () => { 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()) @@ -287,12 +308,16 @@ suite('Activation Manager', () => { .setup((a) => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); await managerTest.activateWorkspace(resource); await managerTest.activateWorkspace(resource); - verify(activationService1.activate(resource)).once(); - verify(activationService2.activate(resource)).once(); autoSelection.verifyAll(); appDiagnostics.verifyAll(); }); @@ -300,36 +325,35 @@ suite('Activation Manager', () => { test('If doc opened is not python, return', async () => { const doc = { uri: Uri.parse('doc'), - languageId: 'NOT PYTHON' + languageId: 'NOT PYTHON', }; - managerTest.onDocOpened(doc as any); + managerTest.onDocOpened((doc as unknown) as TextDocument); 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 + languageId: PYTHON_LANGUAGE, }; when(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).thenReturn(''); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - managerTest.onDocOpened(doc as any); + managerTest.onDocOpened((doc as unknown) as TextDocument); verify(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).once(); - verify(workspaceService.getWorkspaceFolder(doc.uri)).never(); + verify(workspaceService.getWorkspaceFolder(doc.uri)).once(); }); test('If workspace corresponding to the doc has already been activated, then do nothing', async () => { const doc = { uri: Uri.parse('doc'), - languageId: PYTHON_LANGUAGE + languageId: PYTHON_LANGUAGE, }; when(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).thenReturn('key'); managerTest.activatedWorkspaces.add('key'); - managerTest.onDocOpened(doc as any); + managerTest.onDocOpened((doc as unknown) as TextDocument); verify(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).once(); verify(workspaceService.getWorkspaceFolder(doc.uri)).never(); @@ -339,6 +363,7 @@ suite('Activation Manager', () => { const disposable1 = typemoq.Mock.ofType<IDisposable>(); const disposable2 = typemoq.Mock.ofType<IDisposable>(); let docOpenedHandler!: (e: TextDocument) => Promise<void>; + // eslint-disable-next-line @typescript-eslint/ban-types let workspaceFoldersChangedHandler!: Function; const documentUri = Uri.file('a'); const document = typemoq.Mock.ofType<TextDocument>(); @@ -350,7 +375,9 @@ suite('Activation Manager', () => { }); documentManager .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((cb) => (docOpenedHandler = cb)) + .callback((cb) => { + docOpenedHandler = cb; + }) .returns(() => disposable2.object) .verifiable(typemoq.Times.once()); @@ -365,7 +392,6 @@ suite('Activation Manager', () => { 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'); @@ -377,244 +403,17 @@ suite('Activation Manager', () => { documentManager.verifyAll(); verify(workspaceService.onDidChangeWorkspaceFolders).once(); verify(workspaceService.workspaceFolders).atLeast(1); - verify(workspaceService.hasWorkspaceFolders).once(); - //Removed no. of folders to one + // 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 deleted file mode 100644 index 6bc984879488..000000000000 --- a/src/test/activation/activationService.unit.test.ts +++ /dev/null @@ -1,964 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - path: '/foo/bar/python', - sysPrefix: '1', - envName: '1', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.Unknown - }; - const interpreter2: PythonInterpreter = { - path: '/foo/baz/python', - sysPrefix: '1', - envName: '2', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.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: PythonInterpreter = { - path: '/foo/bar/python', - sysPrefix: '1', - envName: '1', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.Unknown - }; - const interpreter2: PythonInterpreter = { - path: '/foo/baz/python', - sysPrefix: '1', - envName: '2', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.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 index ca203155078c..4b157f950bf3 100644 --- a/src/test/activation/activeResource.unit.test.ts +++ b/src/test/activation/activeResource.unit.test.ts @@ -11,7 +11,6 @@ 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; @@ -26,10 +25,10 @@ suite('Active resource service', () => { const activeTextEditor = { document: { isUntitled: false, - uri: Uri.parse('a') - } + uri: Uri.parse('a'), + }, }; - // tslint:disable-next-line:no-any + when(documentManager.activeTextEditor).thenReturn(activeTextEditor as any); const activeResource = activeResourceService.getActiveResource(); @@ -43,10 +42,10 @@ suite('Active resource service', () => { const activeTextEditor = { document: { isUntitled: true, - uri: Uri.parse('a') - } + uri: Uri.parse('a'), + }, }; - // tslint:disable-next-line:no-any + when(documentManager.activeTextEditor).thenReturn(activeTextEditor as any); when(workspaceService.workspaceFolders).thenReturn([]); @@ -60,14 +59,14 @@ suite('Active resource service', () => { 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('a'), }, { - uri: Uri.parse('b') - } + 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(); diff --git a/src/test/activation/defaultLanguageServer.unit.test.ts b/src/test/activation/defaultLanguageServer.unit.test.ts new file mode 100644 index 000000000000..a06a146b9e32 --- /dev/null +++ b/src/test/activation/defaultLanguageServer.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { anything, instance, mock, when, verify } from 'ts-mockito'; +import { Extension } from 'vscode'; +import { setDefaultLanguageServer } from '../../client/activation/common/defaultlanguageServer'; +import { LanguageServerType } from '../../client/activation/types'; +import { PYLANCE_EXTENSION_ID } from '../../client/common/constants'; +import { IDefaultLanguageServer, IExtensions } from '../../client/common/types'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceManager } from '../../client/ioc/types'; + +suite('Activation - setDefaultLanguageServer()', () => { + let extensions: IExtensions; + let extension: Extension<unknown>; + let serviceManager: IServiceManager; + setup(() => { + extensions = mock(); + extension = mock(); + serviceManager = mock(ServiceManager); + }); + + test('Pylance not installed', async () => { + let defaultServerType; + + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(undefined); + when(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).thenCall( + (_symbol, value: IDefaultLanguageServer) => { + defaultServerType = value.defaultLSType; + }, + ); + + await setDefaultLanguageServer(instance(extensions), instance(serviceManager)); + + verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); + verify(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).once(); + expect(defaultServerType).to.equal(LanguageServerType.Jedi); + }); + + test('Pylance installed', async () => { + let defaultServerType; + + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(extension)); + when(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).thenCall( + (_symbol, value: IDefaultLanguageServer) => { + defaultServerType = value.defaultLSType; + }, + ); + + await setDefaultLanguageServer(instance(extensions), instance(serviceManager)); + + verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); + verify(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).once(); + expect(defaultServerType).to.equal(LanguageServerType.Node); + }); +}); diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index d49c566df0bf..a89797bfebef 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -8,38 +8,38 @@ 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 { IApplicationEnvironment, IApplicationShell, IWorkspaceService } 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, + IExperimentService, IPersistentState, IPersistentStateFactory, - IRandom + 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'; +import { WorkspaceConfiguration } from 'vscode'; -// 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 experiments: TypeMoq.IMock<IExperimentService>; 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; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + setup(() => { - experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); + experiments = TypeMoq.Mock.ofType<IExperimentService>(); appShell = TypeMoq.Mock.ofType<IApplicationShell>(); browserService = TypeMoq.Mock.ofType<IBrowserService>(); random = TypeMoq.Mock.ofType<IRandom>(); @@ -48,15 +48,16 @@ suite('Extension survey prompt - shouldShowBanner()', () => { doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>(); platformService = TypeMoq.Mock.ofType<IPlatformService>(); appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); when( persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).thenReturn(disableSurveyForTime.object); when( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).thenReturn(doNotShowAgain.object); extensionSurveyPrompt = new ExtensionSurveyPrompt( appShell.object, @@ -66,7 +67,8 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, - 10 + workspaceService.object, + 10, ); }); test('Returns false if do not show again is clicked', async () => { @@ -83,11 +85,11 @@ suite('Extension survey prompt - shouldShowBanner()', () => { persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).never(); verify( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).once(); random.verifyAll(); }); @@ -106,11 +108,11 @@ suite('Extension survey prompt - shouldShowBanner()', () => { persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).once(); verify( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).once(); random.verifyAll(); }); @@ -125,6 +127,40 @@ suite('Extension survey prompt - shouldShowBanner()', () => { } random.verifyAll(); }); + test('Returns true if telemetry.feedback.enabled is enabled', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const telemetryConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('telemetry')).returns(() => telemetryConfig.object); + telemetryConfig + .setup((t) => t.get(TypeMoq.It.isValue('feedback.enabled'), TypeMoq.It.isValue(true))) + .returns(() => true); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(true, 'Banner should be shown when telemetry.feedback.enabled is true'); + workspaceService.verify((w) => w.getConfiguration('telemetry'), TypeMoq.Times.once()); + telemetryConfig.verify((t) => t.get('feedback.enabled', true), TypeMoq.Times.once()); + }); + + test('Returns false if telemetry.feedback.enabled is disabled', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const telemetryConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('telemetry')).returns(() => telemetryConfig.object); + telemetryConfig + .setup((t) => t.get(TypeMoq.It.isValue('feedback.enabled'), TypeMoq.It.isValue(true))) + .returns(() => false); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown when feedback.enabled is false'); + workspaceService.verify((w) => w.getConfiguration('telemetry'), TypeMoq.Times.once()); + telemetryConfig.verify((t) => t.get('feedback.enabled', true), TypeMoq.Times.once()); + }); + 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); @@ -145,7 +181,8 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, - 100 + workspaceService.object, + 100, ); disableSurveyForTime.setup((d) => d.value).returns(() => false); doNotShowAgain.setup((d) => d.value).returns(() => false); @@ -165,7 +202,8 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, - 0 + workspaceService.object, + 0, ); disableSurveyForTime.setup((d) => d.value).returns(() => false); doNotShowAgain.setup((d) => d.value).returns(() => false); @@ -178,9 +216,8 @@ suite('Extension survey prompt - shouldShowBanner()', () => { }); }); -// tslint:disable-next-line: max-func-body-length suite('Extension survey prompt - showSurvey()', () => { - let experiments: TypeMoq.IMock<IExperimentsManager>; + let experiments: TypeMoq.IMock<IExperimentService>; let appShell: TypeMoq.IMock<IApplicationShell>; let browserService: TypeMoq.IMock<IBrowserService>; let random: TypeMoq.IMock<IRandom>; @@ -190,6 +227,7 @@ suite('Extension survey prompt - showSurvey()', () => { let platformService: TypeMoq.IMock<IPlatformService>; let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; let extensionSurveyPrompt: ExtensionSurveyPrompt; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; setup(() => { appShell = TypeMoq.Mock.ofType<IApplicationShell>(); browserService = TypeMoq.Mock.ofType<IBrowserService>(); @@ -199,17 +237,18 @@ suite('Extension survey prompt - showSurvey()', () => { doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>(); platformService = TypeMoq.Mock.ofType<IPlatformService>(); appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); when( persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).thenReturn(disableSurveyForTime.object); when( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).thenReturn(doNotShowAgain.object); - experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); + experiments = TypeMoq.Mock.ofType<IExperimentService>(); extensionSurveyPrompt = new ExtensionSurveyPrompt( appShell.object, browserService.object, @@ -218,19 +257,16 @@ suite('Extension survey prompt - showSurvey()', () => { experiments.object, appEnvironment.object, platformService.object, - 10 + workspaceService.object, + 10, ); }); test("Launch survey if 'Yes' option is clicked", async () => { const packageJson = { - version: 'extensionVersion' + version: 'extensionVersion', }; - const prompts = [ - ExtensionSurveyBanner.bannerLabelYes(), - ExtensionSurveyBanner.maybeLater(), - Common.doNotShowAgain() - ]; + 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) @@ -249,8 +285,8 @@ suite('Extension survey prompt - showSurvey()', () => { .returns(() => OSType.Windows) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) - .returns(() => Promise.resolve(ExtensionSurveyBanner.bannerLabelYes())) + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts)) + .returns(() => Promise.resolve(ExtensionSurveyBanner.bannerLabelYes)) .verifiable(TypeMoq.Times.once()); browserService .setup((s) => s.launch(expectedUrl)) @@ -271,11 +307,11 @@ suite('Extension survey prompt - showSurvey()', () => { persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).once(); verify( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).never(); appShell.verifyAll(); browserService.verifyAll(); @@ -286,15 +322,11 @@ suite('Extension survey prompt - showSurvey()', () => { }); test("Do nothing if 'Maybe later' option is clicked", async () => { - const prompts = [ - ExtensionSurveyBanner.bannerLabelYes(), - ExtensionSurveyBanner.maybeLater(), - Common.doNotShowAgain() - ]; + 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())) + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts)) + .returns(() => Promise.resolve(ExtensionSurveyBanner.maybeLater)) .verifiable(TypeMoq.Times.once()); browserService .setup((s) => s.launch(TypeMoq.It.isAny())) @@ -315,11 +347,11 @@ suite('Extension survey prompt - showSurvey()', () => { persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).never(); verify( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).never(); appShell.verifyAll(); browserService.verifyAll(); @@ -329,14 +361,10 @@ suite('Extension survey prompt - showSurvey()', () => { }); test('Do nothing if no option is clicked', async () => { - const prompts = [ - ExtensionSurveyBanner.bannerLabelYes(), - ExtensionSurveyBanner.maybeLater(), - Common.doNotShowAgain() - ]; + 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)) + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); browserService @@ -358,11 +386,11 @@ suite('Extension survey prompt - showSurvey()', () => { persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).never(); verify( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).never(); appShell.verifyAll(); browserService.verifyAll(); @@ -371,16 +399,12 @@ suite('Extension survey prompt - showSurvey()', () => { platformService.verifyAll(); }); - test("Disable prompt if 'Do not show again' option is clicked", async () => { - const prompts = [ - ExtensionSurveyBanner.bannerLabelYes(), - ExtensionSurveyBanner.maybeLater(), - Common.doNotShowAgain() - ]; + test('Disable prompt if "Don\'t 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())) + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts)) + .returns(() => Promise.resolve(Common.doNotShowAgain)) .verifiable(TypeMoq.Times.once()); browserService .setup((s) => s.launch(TypeMoq.It.isAny())) @@ -401,11 +425,11 @@ suite('Extension survey prompt - showSurvey()', () => { persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, false, - anything() - ) + anything(), + ), ).never(); verify( - persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), ).once(); appShell.verifyAll(); browserService.verifyAll(); @@ -415,7 +439,6 @@ suite('Extension survey prompt - showSurvey()', () => { }); }); -// tslint:disable-next-line: max-func-body-length suite('Extension survey prompt - activate()', () => { let appShell: TypeMoq.IMock<IApplicationShell>; let browserService: TypeMoq.IMock<IBrowserService>; @@ -423,28 +446,29 @@ suite('Extension survey prompt - activate()', () => { let persistentStateFactory: IPersistentStateFactory; let shouldShowBanner: sinon.SinonStub<any>; let showSurvey: sinon.SinonStub<any>; - let experiments: TypeMoq.IMock<IExperimentsManager>; + let experiments: TypeMoq.IMock<IExperimentService>; let extensionSurveyPrompt: ExtensionSurveyPrompt; let platformService: TypeMoq.IMock<IPlatformService>; let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; setup(() => { appShell = TypeMoq.Mock.ofType<IApplicationShell>(); browserService = TypeMoq.Mock.ofType<IBrowserService>(); random = TypeMoq.Mock.ofType<IRandom>(); persistentStateFactory = mock(PersistentStateFactory); - experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); + experiments = TypeMoq.Mock.ofType<IExperimentService>(); platformService = TypeMoq.Mock.ofType<IPlatformService>(); appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); }); teardown(() => { sinon.restore(); }); - test("If user is not in 'ShowExtensionPrompt' experiment, send telemetry if in control group & return", async () => { + test("If user is not in 'ShowExtensionSurveyPrompt' experiment, return immediately", async () => { shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); shouldShowBanner.callsFake(() => false); - showSurvey = sinon.stub(ExtensionSurveyPrompt.prototype, 'showSurvey'); extensionSurveyPrompt = new ExtensionSurveyPrompt( appShell.object, browserService.object, @@ -453,22 +477,19 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, - 10 + workspaceService.object, + 10, ); experiments - .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.enabled)) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - experiments - .setup((exp) => exp.sendTelemetryIfInExperiment(ShowExtensionSurveyPrompt.control)) - .returns(() => undefined) + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.experiment)) + .returns(() => Promise.resolve(false)) .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 () => { + test("No survey is shown if shouldShowBanner() returns false and user is in 'ShowExtensionSurveyPrompt' experiment", async () => { const deferred = createDeferred<true>(); shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); shouldShowBanner.callsFake(() => false); @@ -486,17 +507,14 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, - 50 + 50, ); experiments - .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.enabled)) - .returns(() => true) + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.experiment)) + .returns(() => Promise.resolve(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); @@ -506,7 +524,7 @@ suite('Extension survey prompt - activate()', () => { experiments.verifyAll(); }); - test("Survey is shown after waitTimeToShowSurvey if shouldShowBanner() returns true and user is in 'ShowExtensionPrompt' experiment", async () => { + test("Survey is shown after waitTimeToShowSurvey if shouldShowBanner() returns true and user is in 'ShowExtensionSurveyPrompt' experiment", async () => { const deferred = createDeferred<true>(); shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); shouldShowBanner.callsFake(() => true); @@ -524,17 +542,14 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, - 50 + 50, ); experiments - .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.enabled)) - .returns(() => true) + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.experiment)) + .returns(() => Promise.resolve(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); diff --git a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts new file mode 100644 index 000000000000..66cb9e0ae604 --- /dev/null +++ b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { EventEmitter, Uri, WorkspaceFolder } from 'vscode'; +import { JediLanguageServerAnalysisOptions } from '../../../client/activation/jedi/analysisOptions'; +import { ILanguageServerAnalysisOptions, 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 { IConfigurationService } from '../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { Architecture } from '../../../client/common/utils/platform'; + +suite('Jedi LSP - analysis Options', () => { + const workspacePath = path.join('this', 'is', 'fake', 'workspace', 'path'); + const expectedWorkspacePath = path.sep + workspacePath; + + let envVarsProvider: IEnvironmentVariablesProvider; + let lsOutputChannel: ILanguageServerOutputChannel; + let configurationService: IConfigurationService; + let workspaceService: IWorkspaceService; + + let analysisOptions: ILanguageServerAnalysisOptions; + + class MockWorkspaceFolder implements WorkspaceFolder { + public uri: Uri; + + public name: string; + + public ownedResources = new Set<string>(); + + constructor(folder: string, public index: number = 0) { + this.uri = Uri.file(folder); + this.name = folder; + } + } + + setup(() => { + envVarsProvider = mock(IEnvironmentVariablesProvider); + lsOutputChannel = mock(ILanguageServerOutputChannel); + configurationService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + + const onDidChangeEnvVariables = new EventEmitter<Uri | undefined>(); + when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event); + + analysisOptions = new JediLanguageServerAnalysisOptions( + instance(envVarsProvider), + instance(lsOutputChannel), + instance(configurationService), + instance(workspaceService), + ); + }); + + test('Validate defaults', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(configurationService.getSettings(anything())).thenReturn({} as any); + analysisOptions.initialize(undefined, undefined); + + const result = await analysisOptions.getAnalysisOptions(); + + expect(result.initializationOptions.markupKindPreferred).to.deep.equal('markdown'); + expect(result.initializationOptions.completion.resolveEagerly).to.deep.equal(false); + expect(result.initializationOptions.completion.disableSnippets).to.deep.equal(true); + expect(result.initializationOptions.diagnostics.enable).to.deep.equal(true); + expect(result.initializationOptions.diagnostics.didOpen).to.deep.equal(true); + expect(result.initializationOptions.diagnostics.didSave).to.deep.equal(true); + expect(result.initializationOptions.diagnostics.didChange).to.deep.equal(true); + expect(result.initializationOptions.hover.disable.keyword.all).to.deep.equal(true); + expect(result.initializationOptions.workspace.extraPaths).to.deep.equal([]); + expect(result.initializationOptions.workspace.symbols.maxSymbols).to.deep.equal(0); + expect(result.initializationOptions.semantic_tokens.enable).to.deep.equal(true); + }); + + test('With interpreter path', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(configurationService.getSettings(anything())).thenReturn({} as any); + const pythonEnvironment: PythonEnvironment = { + envPath: '.../.venv', + id: 'base_env', + envType: EnvironmentType.Conda, + path: '.../.venv/bin/python', + architecture: Architecture.x86, + sysPrefix: 'prefix/path', + }; + analysisOptions.initialize(undefined, pythonEnvironment); + + const result = await analysisOptions.getAnalysisOptions(); + + expect(result.initializationOptions.workspace.environmentPath).to.deep.equal('.../.venv/bin/python'); + }); + + test('Without extraPaths provided and no workspace', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(configurationService.getSettings(anything())).thenReturn({} as any); + analysisOptions.initialize(undefined, undefined); + + const result = await analysisOptions.getAnalysisOptions(); + expect(result.initializationOptions.workspace.extraPaths).to.deep.equal([]); + }); + + test('Without extraPaths provided', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(new MockWorkspaceFolder(workspacePath)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(configurationService.getSettings(anything())).thenReturn({} as any); + analysisOptions.initialize(undefined, undefined); + + const result = await analysisOptions.getAnalysisOptions(); + expect(result.initializationOptions.workspace.extraPaths).to.deep.equal([expectedWorkspacePath]); + }); + + test('With extraPaths provided', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(new MockWorkspaceFolder(workspacePath)); + when(configurationService.getSettings(anything())).thenReturn({ + // We expect a distinct set of paths back, using __dirname to test absolute path + autoComplete: { extraPaths: [__dirname, 'relative/pathB', 'relative/pathB'] }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + analysisOptions.initialize(undefined, undefined); + + const result = await analysisOptions.getAnalysisOptions(); + + expect(result.initializationOptions.workspace.extraPaths).to.deep.equal([ + expectedWorkspacePath, + __dirname, + path.join(expectedWorkspacePath, 'relative/pathB'), + ]); + }); +}); diff --git a/src/test/activation/languageServer/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts deleted file mode 100644 index 13452eac1356..000000000000 --- a/src/test/activation/languageServer/activator.unit.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -// 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 deleted file mode 100644 index 34d12c454939..000000000000 --- a/src/test/activation/languageServer/analysisOptions.unit.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -// 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 deleted file mode 100644 index 2b7c09eddb9e..000000000000 --- a/src/test/activation/languageServer/downloadChannelRules.unit.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -// 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 deleted file mode 100644 index fa72ee5e8cf9..000000000000 --- a/src/test/activation/languageServer/downloader.unit.test.ts +++ /dev/null @@ -1,391 +0,0 @@ -// 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 deleted file mode 100644 index 5b1b73ffe03e..000000000000 --- a/src/test/activation/languageServer/languageClientFactory.unit.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -// 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 deleted file mode 100644 index ba13b59f7c1f..000000000000 --- a/src/test/activation/languageServer/languageServer.unit.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -// 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 deleted file mode 100644 index 8ea004ed6225..000000000000 --- a/src/test/activation/languageServer/languageServerCompatibilityService.unit.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index d18bacd66e3d..000000000000 --- a/src/test/activation/languageServer/languageServerExtension.unit.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index 381ff514c4ad..000000000000 --- a/src/test/activation/languageServer/languageServerFolderService.unit.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -// 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 deleted file mode 100644 index e32bc7acbf82..000000000000 --- a/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 1101876781e2..000000000000 --- a/src/test/activation/languageServer/languageServerPackageService.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -// 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 deleted file mode 100644 index 358b48f63c41..000000000000 --- a/src/test/activation/languageServer/languageServerPackageService.unit.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -// 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 deleted file mode 100644 index 09a24a0df6d0..000000000000 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -// 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/platformData.unit.test.ts b/src/test/activation/languageServer/platformData.unit.test.ts deleted file mode 100644 index d424aa63ceac..000000000000 --- a/src/test/activation/languageServer/platformData.unit.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -// 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 deleted file mode 100644 index 3b87588774eb..000000000000 --- a/src/test/activation/node/activator.unit.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -// 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/analysisOptions.unit.test.ts b/src/test/activation/node/analysisOptions.unit.test.ts new file mode 100644 index 000000000000..d5e97f93768e --- /dev/null +++ b/src/test/activation/node/analysisOptions.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 typemoq from 'typemoq'; +import { WorkspaceFolder } from 'vscode'; +import { DocumentFilter } from 'vscode-languageclient/node'; + +import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/node/analysisOptions'; +import { ILanguageServerOutputChannel } from '../../../client/activation/types'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { ILogOutputChannel } from '../../../client/common/types'; + +suite('Pylance Language Server - Analysis Options', () => { + class TestClass extends NodeLanguageServerAnalysisOptions { + public getWorkspaceFolder(): WorkspaceFolder | undefined { + return super.getWorkspaceFolder(); + } + + public getDocumentFilters(workspaceFolder?: WorkspaceFolder): DocumentFilter[] { + return super.getDocumentFilters(workspaceFolder); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async getInitializationOptions(): Promise<any> { + return super.getInitializationOptions(); + } + } + + let analysisOptions: TestClass; + let outputChannel: ILogOutputChannel; + let lsOutputChannel: typemoq.IMock<ILanguageServerOutputChannel>; + let workspace: typemoq.IMock<IWorkspaceService>; + + setup(() => { + outputChannel = typemoq.Mock.ofType<ILogOutputChannel>().object; + workspace = typemoq.Mock.ofType<IWorkspaceService>(); + workspace.setup((w) => w.isVirtualWorkspace).returns(() => false); + lsOutputChannel = typemoq.Mock.ofType<ILanguageServerOutputChannel>(); + lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel); + analysisOptions = new TestClass(lsOutputChannel.object, workspace.object); + }); + + test('Workspace folder is undefined', () => { + const workspaceFolder = analysisOptions.getWorkspaceFolder(); + expect(workspaceFolder).to.be.equal(undefined); + }); + + test('Document filter matches expected python language schemes', () => { + const filter = analysisOptions.getDocumentFilters(); + expect(filter).to.be.equal(PYTHON); + }); + + test('Document filter matches all python language schemes when in virtual workspace', () => { + workspace.reset(); + workspace.setup((w) => w.isVirtualWorkspace).returns(() => true); + const filter = analysisOptions.getDocumentFilters(); + assert.deepEqual(filter, [{ language: PYTHON_LANGUAGE }]); + }); + + test('Initialization options include experimentation capability', async () => { + const options = await analysisOptions.getInitializationOptions(); + expect(options?.experimentationSupport).to.be.equal(true); + }); +}); diff --git a/src/test/activation/node/languageServerChangeHandler.unit.test.ts b/src/test/activation/node/languageServerChangeHandler.unit.test.ts index 94b7ef180311..7f1dffaf848b 100644 --- a/src/test/activation/node/languageServerChangeHandler.unit.test.ts +++ b/src/test/activation/node/languageServerChangeHandler.unit.test.ts @@ -3,34 +3,31 @@ 'use strict'; -import { anyString, instance, mock, verify, when } from 'ts-mockito'; -import { EventEmitter, Extension } from 'vscode'; +import { anyString, instance, mock, verify, when, anything } from 'ts-mockito'; +import { ConfigurationTarget, EventEmitter, WorkspaceConfiguration } 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 { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; -import { IExtensions } from '../../../client/common/types'; +import { IConfigurationService, 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>; + let workspace: IWorkspaceService; + let configService: IConfigurationService; + 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'); + workspace = mock<IWorkspaceService>(); + configService = mock<IConfigurationService>(); extensionsChangedEvent = new EventEmitter<void>(); when(extensions.onDidChange).thenReturn(extensionsChangedEvent.event); @@ -40,141 +37,158 @@ suite('Language Server - Change Handler', () => { handler?.dispose(); }); - [undefined, LanguageServerType.None, LanguageServerType.Microsoft, LanguageServerType.Node].forEach(async (t) => { + [undefined, LanguageServerType.None, LanguageServerType.Jedi, 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() - ) + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), ).thenReturn(Promise.resolve(undefined)); handler = makeHandler(undefined); await handler.handleLanguageServerChange(LanguageServerType.Node); - verify( - appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()) - ).never(); + verify(appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange, Common.reload)).never(); verify( appShell.showWarningMessage( - Pylance.installPylanceMessage(), - Common.bannerLabelYes(), - Common.bannerLabelNo() - ) + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), ).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())); + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).thenReturn(Promise.resolve(Pylance.pylanceInstallPylance)); handler = makeHandler(undefined); await handler.handleLanguageServerChange(LanguageServerType.Node); - verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).once(); + verify(commands.executeCommand('extension.open', 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())); + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).thenReturn(Promise.resolve(Pylance.remindMeLater)); handler = makeHandler(undefined); await handler.handleLanguageServerChange(LanguageServerType.Node); - verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).never(); + verify(commands.executeCommand('extension.open', 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(); + [ConfigurationTarget.Global, ConfigurationTarget.Workspace].forEach((target) => { + const targetName = target === ConfigurationTarget.Global ? 'global' : 'workspace'; + test(`Revert to Jedi with setting in ${targetName} config`, async () => { + const configuration = mock<WorkspaceConfiguration>(); + + when( + appShell.showWarningMessage( + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).thenReturn(Promise.resolve(Pylance.pylanceRevertToJedi)); + + when(workspace.getConfiguration('python')).thenReturn(instance(configuration)); + + const inspection = { + key: 'python.languageServer', + workspaceValue: target === ConfigurationTarget.Workspace ? LanguageServerType.Node : undefined, + globalValue: target === ConfigurationTarget.Global ? LanguageServerType.Node : undefined, + }; + + when(configuration.inspect<string>('languageServer')).thenReturn(inspection); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange, Common.reload), + ).never(); + verify( + appShell.showWarningMessage( + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).once(); + verify(configService.updateSetting('languageServer', LanguageServerType.Jedi, undefined, target)).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); + [ConfigurationTarget.WorkspaceFolder, undefined].forEach((target) => { + const targetName = target === ConfigurationTarget.WorkspaceFolder ? 'workspace folder' : 'missing'; + test(`Revert to Jedi with ${targetName} setting does nothing`, async () => { + const configuration = mock<WorkspaceConfiguration>(); - when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(pylanceExtension); - extensionsChangedEvent.fire(); + when( + appShell.showWarningMessage( + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).thenReturn(Promise.resolve(Pylance.pylanceRevertToJedi)); - await handler.pylanceInstallCompleted; - verify(commands.executeCommand('workbench.action.reloadWindow')).never(); + when(workspace.getConfiguration('python')).thenReturn(instance(configuration)); + + const inspection = { + key: 'python.languageServer', + workspaceFolderValue: + target === ConfigurationTarget.WorkspaceFolder ? LanguageServerType.Node : undefined, + }; + + when(configuration.inspect<string>('languageServer')).thenReturn(inspection); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange, Common.reload), + ).never(); + verify( + appShell.showWarningMessage( + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).once(); + verify(configService.updateSetting(anything(), anything(), anything(), anything())).never(); + }); }); function makeHandler(initialLSType: LanguageServerType | undefined): LanguageServerChangeHandler { @@ -182,8 +196,9 @@ suite('Language Server - Change Handler', () => { initialLSType, instance(extensions), instance(appShell), - instance(appEnv), - instance(commands) + instance(commands), + instance(workspace), + instance(configService), ); } }); diff --git a/src/test/activation/node/languageServerFolderService.unit.test.ts b/src/test/activation/node/languageServerFolderService.unit.test.ts deleted file mode 100644 index b944dde8ffb6..000000000000 --- a/src/test/activation/node/languageServerFolderService.unit.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -// 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/languageServer/outputChannel.unit.test.ts b/src/test/activation/outputChannel.unit.test.ts similarity index 78% rename from src/test/activation/languageServer/outputChannel.unit.test.ts rename to src/test/activation/outputChannel.unit.test.ts index c120cfdc45ca..f8f38783bb0e 100644 --- a/src/test/activation/languageServer/outputChannel.unit.test.ts +++ b/src/test/activation/outputChannel.unit.test.ts @@ -5,31 +5,30 @@ 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'; +import { LanguageServerOutputChannel } from '../../client/activation/common/outputChannel'; +import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; +import { ILogOutputChannel } 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>; + let output: TypeMoq.IMock<ILogOutputChannel>; setup(() => { appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - output = TypeMoq.Mock.ofType<IOutputChannel>(); + output = TypeMoq.Mock.ofType<ILogOutputChannel>(); commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - languageServerOutputChannel = new LanguageServerOutputChannel(appShell.object, commandManager.object); + 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())) + .setup((a) => a.createOutputChannel(OutputChannelNames.languageServer)) .returns(() => output.object) .verifiable(TypeMoq.Times.once()); - const channel = languageServerOutputChannel.channel; + const { channel } = languageServerOutputChannel; appShell.verifyAll(); expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); }); @@ -40,7 +39,7 @@ suite('Language Server Output Channel', () => { .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) .returns(() => output.object) .verifiable(TypeMoq.Times.never()); - const channel = languageServerOutputChannel.channel; + const { channel } = languageServerOutputChannel; appShell.verifyAll(); expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); }); @@ -54,8 +53,8 @@ suite('Language Server Output Channel', () => { c.executeCommand( TypeMoq.It.isValue('setContext'), TypeMoq.It.isValue('python.hasLanguageServerOutputChannel'), - TypeMoq.It.isValue(true) - ) + TypeMoq.It.isValue(true), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -63,8 +62,8 @@ suite('Language Server Output Channel', () => { .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; + // Doesn't matter how many times we access channel property. + let { channel } = languageServerOutputChannel; channel = languageServerOutputChannel.channel; channel = languageServerOutputChannel.channel; @@ -75,7 +74,9 @@ suite('Language Server Output Channel', () => { expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); }); test('Display panel when invoking command python.viewLanguageServerOutput', async () => { - let cmdCallback: Function | undefined; + let cmdCallback: () => unknown | undefined = () => { + /* no-op */ + }; appShell .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) .returns(() => output.object) @@ -85,18 +86,20 @@ suite('Language Server Output Channel', () => { c.executeCommand( TypeMoq.It.isValue('setContext'), TypeMoq.It.isValue('python.hasLanguageServerOutputChannel'), - TypeMoq.It.isValue(true) - ) + 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)) + .callback((_: string, callback: () => unknown) => { + 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; + // Doesn't matter how many times we access channel property. + let { channel } = languageServerOutputChannel; channel = languageServerOutputChannel.channel; channel = languageServerOutputChannel.channel; diff --git a/src/test/activation/partialModeStatus.unit.test.ts b/src/test/activation/partialModeStatus.unit.test.ts new file mode 100644 index 000000000000..12e4b6fc0c5b --- /dev/null +++ b/src/test/activation/partialModeStatus.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 rewiremock from 'rewiremock'; +import * as typemoq from 'typemoq'; +import * as vscodeTypes from 'vscode'; +import { DocumentSelector, LanguageStatusItem } from 'vscode'; +import { PartialModeStatusItem } from '../../client/activation/partialModeStatus'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { IDisposableRegistry } from '../../client/common/types'; +import { Common, LanguageService } from '../../client/common/utils/localize'; + +suite('Partial Mode Status', async () => { + let workspaceService: typemoq.IMock<IWorkspaceService>; + let actualSelector: DocumentSelector | undefined; + let languageItem: LanguageStatusItem; + let vscodeMock: typeof vscodeTypes; + setup(() => { + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + languageItem = ({ + name: '', + severity: 2, + text: '', + detail: undefined, + command: undefined, + } as unknown) as LanguageStatusItem; + actualSelector = undefined; + vscodeMock = ({ + languages: { + createLanguageStatusItem: (_: string, selector: DocumentSelector) => { + actualSelector = selector; + return languageItem; + }, + }, + LanguageStatusSeverity: { + Information: 0, + Warning: 1, + Error: 2, + }, + Uri: { + parse: (s: string) => s, + }, + } as unknown) as typeof vscodeTypes; + rewiremock.enable(); + rewiremock('vscode').with(vscodeMock); + }); + + teardown(() => { + rewiremock.disable(); + }); + + test("No item is created if workspace is trusted and isn't virtual", async () => { + workspaceService.setup((w) => w.isTrusted).returns(() => true); + workspaceService.setup((w) => w.isVirtualWorkspace).returns(() => false); + const quickFixService = new PartialModeStatusItem( + workspaceService.object, + typemoq.Mock.ofType<IDisposableRegistry>().object, + ); + + await quickFixService.activate(); + + assert.deepEqual(actualSelector, undefined); + }); + + test('Expected status item is created if workspace is not trusted', async () => { + workspaceService.setup((w) => w.isTrusted).returns(() => false); + workspaceService.setup((w) => w.isVirtualWorkspace).returns(() => false); + const statusItem = new PartialModeStatusItem( + workspaceService.object, + typemoq.Mock.ofType<IDisposableRegistry>().object, + ); + + await statusItem.activate(); + + assert.deepEqual(actualSelector!, { + language: 'python', + }); + assert.deepEqual(languageItem, ({ + name: LanguageService.statusItem.name, + severity: vscodeMock.LanguageStatusSeverity.Warning, + text: LanguageService.statusItem.text, + detail: LanguageService.statusItem.detail, + command: { + title: Common.learnMore, + command: 'vscode.open', + arguments: ['https://aka.ms/AAdzyh4'], + }, + } as unknown) as LanguageStatusItem); + }); + + test('Expected status item is created if workspace is virtual', async () => { + workspaceService.setup((w) => w.isTrusted).returns(() => true); + workspaceService.setup((w) => w.isVirtualWorkspace).returns(() => true); + const statusItem = new PartialModeStatusItem( + workspaceService.object, + typemoq.Mock.ofType<IDisposableRegistry>().object, + ); + + await statusItem.activate(); + + assert.deepEqual(actualSelector!, { + language: 'python', + }); + assert.deepEqual(languageItem, ({ + name: LanguageService.statusItem.name, + severity: vscodeMock.LanguageStatusSeverity.Warning, + text: LanguageService.statusItem.text, + detail: LanguageService.virtualWorkspaceStatusItem.detail, + command: { + title: Common.learnMore, + command: 'vscode.open', + arguments: ['https://aka.ms/AAdzyh4'], + }, + } as unknown) as LanguageStatusItem); + }); + + test('Expected status item is created if workspace is both virtual and untrusted', async () => { + workspaceService.setup((w) => w.isTrusted).returns(() => false); + workspaceService.setup((w) => w.isVirtualWorkspace).returns(() => true); + const statusItem = new PartialModeStatusItem( + workspaceService.object, + typemoq.Mock.ofType<IDisposableRegistry>().object, + ); + + await statusItem.activate(); + + assert.deepEqual(actualSelector!, { + language: 'python', + }); + assert.deepEqual(languageItem, ({ + name: LanguageService.statusItem.name, + severity: vscodeMock.LanguageStatusSeverity.Warning, + text: LanguageService.statusItem.text, + detail: LanguageService.statusItem.detail, + command: { + title: Common.learnMore, + command: 'vscode.open', + arguments: ['https://aka.ms/AAdzyh4'], + }, + } as unknown) as LanguageStatusItem); + }); +}); diff --git a/src/test/activation/requirementsTxtLinkActivator.unit.test.ts b/src/test/activation/requirementsTxtLinkActivator.unit.test.ts new file mode 100644 index 000000000000..ebea4af29182 --- /dev/null +++ b/src/test/activation/requirementsTxtLinkActivator.unit.test.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { generatePyPiLink } from '../../client/activation/requirementsTxtLinkActivator'; + +suite('Link to PyPi in requiements test', () => { + [ + ['pytest', 'pytest'], + ['pytest-cov', 'pytest-cov'], + ['pytest_cov', 'pytest_cov'], + ['pytest_cov[an_extra]', 'pytest_cov'], + ['pytest == 0.6.1', 'pytest'], + ['pytest== 0.6.1', 'pytest'], + ['requests [security] >= 2.8.1, == 2.8.* ; python_version < "2.7"', 'requests'], + ['# a comment', null], + ['', null], + ].forEach(([input, expected]) => { + test(`PyPI link case: "${input}"`, () => { + expect(generatePyPiLink(input!)).equal(expected ? `https://pypi.org/project/${expected}/` : null); + }); + }); +}); diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index 51219665453e..177eae810810 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -2,64 +2,19 @@ // 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 { LanguageServerOutputChannel } from '../../client/activation/common/outputChannel'; 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 +import { LoadLanguageServerExtension } from '../../client/activation/common/loadLanguageServerExtension'; +import { RequirementsTxtLinkActivator } from '../../client/activation/requirementsTxtLinkActivator'; suite('Unit Tests - Language Server Activation Service Registry', () => { let serviceManager: IServiceManager; @@ -68,147 +23,35 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { serviceManager = mock(ServiceManager); }); - // tslint:disable-next-line: max-func-body-length - test('Ensure services are registered', async () => { - registerTypes(instance(serviceManager), LanguageServerType.Microsoft); + test('Ensure common services are registered', async () => { + registerTypes(instance(serviceManager)); 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 - ) + serviceManager.add<IExtensionActivationManager>(IExtensionActivationManager, ExtensionActivationManager), ).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 - ) + serviceManager.addSingleton<ILanguageServerOutputChannel>( + ILanguageServerOutputChannel, + LanguageServerOutputChannel, + ), ).once(); - verify(serviceManager.add<ILanguageServerProxy>(ILanguageServerProxy, DotNetLanguageServerProxy)).once(); - verify(serviceManager.add<ILanguageServerManager>(ILanguageServerManager, DotNetLanguageServerManager)).once(); verify( - serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, AATesting) + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ExtensionSurveyPrompt, + ), ).once(); verify( - serviceManager.addSingleton<ILanguageServerOutputChannel>( - ILanguageServerOutputChannel, - LanguageServerOutputChannel - ) + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + LoadLanguageServerExtension, + ), ).once(); verify( serviceManager.addSingleton<IExtensionSingleActivationService>( IExtensionSingleActivationService, - ExtensionSurveyPrompt - ) + RequirementsTxtLinkActivator, + ), ).once(); }); }); diff --git a/src/test/analysisEngineTest.ts b/src/test/analysisEngineTest.ts index 5a68406b9f32..90e433f91647 100644 --- a/src/test/analysisEngineTest.ts +++ b/src/test/analysisEngineTest.ts @@ -3,7 +3,6 @@ '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'); diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts index 58a32defc402..03016956dbef 100644 --- a/src/test/api.functional.test.ts +++ b/src/test/api.functional.test.ts @@ -3,25 +3,31 @@ 'use strict'; -// tslint:disable:no-any max-func-body-length - import { assert, expect } from 'chai'; import * as path from 'path'; +import * as sinon from 'sinon'; 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 { IConfigurationService, IDisposableRegistry } from '../client/common/types'; +import { IEnvironmentVariablesProvider } from '../client/common/variables/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'; +import { IDiscoveryAPI } from '../client/pythonEnvironments/base/locator'; +import * as pythonDebugger from '../client/debugger/pythonDebugger'; +import { + JupyterExtensionIntegration, + JupyterExtensionPythonEnvironments, + JupyterPythonEnvironmentApi, +} from '../client/jupyter/jupyterIntegration'; +import { EventEmitter, Uri } from 'vscode'; suite('Extension API', () => { - const debuggerPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy'); + const debuggerPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'lib', 'python', 'debugpy'); const debuggerHost = 'somehost'; const debuggerPort = 12345; @@ -29,53 +35,44 @@ suite('Extension API', () => { let serviceManager: IServiceManager; let configurationService: IConfigurationService; let interpreterService: IInterpreterService; + let discoverAPI: IDiscoveryAPI; + let environmentVariablesProvider: IEnvironmentVariablesProvider; + let getDebugpyPathStub: sinon.SinonStub; setup(() => { serviceContainer = mock(ServiceContainer); serviceManager = mock(ServiceManager); configurationService = mock(ConfigurationService); interpreterService = mock(InterpreterService); + environmentVariablesProvider = mock<IEnvironmentVariablesProvider>(); + discoverAPI = mock<IDiscoveryAPI>(); + when(discoverAPI.getEnvs()).thenReturn([]); when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( - instance(configurationService) + instance(configurationService), + ); + when(serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider)).thenReturn( + instance(environmentVariablesProvider), + ); + when(serviceContainer.get<JupyterExtensionIntegration>(JupyterExtensionIntegration)).thenReturn( + instance(mock<JupyterExtensionIntegration>()), ); when(serviceContainer.get<IInterpreterService>(IInterpreterService)).thenReturn(instance(interpreterService)); + const onDidChangePythonEnvironment = new EventEmitter<Uri>(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + when(serviceContainer.get<JupyterPythonEnvironmentApi>(JupyterExtensionPythonEnvironments)).thenReturn( + jupyterApi, + ); + when(serviceContainer.get<IDisposableRegistry>(IDisposableRegistry)).thenReturn([]); + getDebugpyPathStub = sinon.stub(pythonDebugger, 'getDebugpyPath'); + getDebugpyPathStub.resolves(debuggerPath); }); - 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); + teardown(() => { + sinon.restore(); }); test('Test debug launcher args (no-wait)', async () => { @@ -84,9 +81,14 @@ suite('Extension API', () => { const args = await buildApi( Promise.resolve(), instance(serviceManager), - instance(serviceContainer) + instance(serviceContainer), + instance(discoverAPI), ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); - const expectedArgs = [debuggerPath.fileToCommandArgument(), '--listen', `${debuggerHost}:${debuggerPort}`]; + const expectedArgs = [ + debuggerPath.fileToCommandArgumentForPythonExt(), + '--listen', + `${debuggerHost}:${debuggerPort}`, + ]; expect(args).to.be.deep.equal(expectedArgs); }); @@ -97,13 +99,14 @@ suite('Extension API', () => { const args = await buildApi( Promise.resolve(), instance(serviceManager), - instance(serviceContainer) + instance(serviceContainer), + instance(discoverAPI), ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); const expectedArgs = [ - debuggerPath.fileToCommandArgument(), + debuggerPath.fileToCommandArgumentForPythonExt(), '--listen', `${debuggerHost}:${debuggerPort}`, - '--wait-for-client' + '--wait-for-client', ]; expect(args).to.be.deep.equal(expectedArgs); @@ -113,9 +116,10 @@ suite('Extension API', () => { const pkgPath = await buildApi( Promise.resolve(), instance(serviceManager), - instance(serviceContainer) + instance(serviceContainer), + instance(discoverAPI), ).debug.getDebuggerPackagePath(); - assert.equal(pkgPath, debuggerPath); + assert.strictEqual(pkgPath, debuggerPath); }); }); diff --git a/src/test/api.test.ts b/src/test/api.test.ts new file mode 100644 index 000000000000..f0813ce16a9b --- /dev/null +++ b/src/test/api.test.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { PythonExtension } from '../client/api/types'; +import { ProposedExtensionAPI } from '../client/proposedApiTypes'; +import { initialize } from './initialize'; + +suite('Python API tests', () => { + let api: PythonExtension & ProposedExtensionAPI; + suiteSetup(async () => { + api = await initialize(); + }); + test('Active environment is defined', async () => { + const environmentPath = api.environments.getActiveEnvironmentPath(); + const environment = await api.environments.resolveEnvironment(environmentPath); + expect(environment).to.not.equal( + undefined, + `Active environment is not defined, envPath: ${JSON.stringify(environmentPath)}, env: ${JSON.stringify( + environment, + )}`, + ); + }); +}); diff --git a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts index 997a9fdd3725..3a2b9c2f62dd 100644 --- a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -12,27 +10,20 @@ 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 { DiagnosticScope, IDiagnostic, IDiagnosticsService } 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 { IWorkspaceService } from '../../../client/common/application/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 workspaceService: typemoq.IMock<IWorkspaceService>; 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; @@ -47,16 +38,18 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { lsNotSupportedCheck.setup((service) => service.runInBackground).returns(() => false); pythonInterpreterCheck = typemoq.Mock.ofType<IDiagnosticsService>(); pythonInterpreterCheck.setup((service) => service.runInBackground).returns(() => false); - outputChannel = typemoq.Mock.ofType<IOutputChannel>(); + pythonInterpreterCheck.setup((service) => service.runInUntrustedWorkspace).returns(() => false); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + workspaceService.setup((w) => w.isTrusted).returns(() => true); 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); + .setup((d) => d.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); - appDiagnostics = new ApplicationDiagnostics(serviceContainer.object, outputChannel.object); + appDiagnostics = new ApplicationDiagnostics(serviceContainer.object); }); teardown(() => { @@ -64,20 +57,30 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { 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); + 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()); - appDiagnostics.register(); + await appDiagnostics.performPreStartupHealthCheck(undefined); - sourceMapService.verifyAll(); + envHealthCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); + pythonInterpreterCheck.verifyAll(); }); - test('Performing Pre Startup Health Check must diagnose all validation checks', async () => { + test('When running in a untrusted workspace skip diagnosing validation checks which do not support it', async () => { + workspaceService.reset(); + workspaceService.setup((w) => w.isTrusted).returns(() => false); envHealthCheck .setup((e) => e.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) @@ -89,7 +92,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { pythonInterpreterCheck .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) - .verifiable(typemoq.Times.once()); + .verifiable(typemoq.Times.never()); await appDiagnostics.performPreStartupHealthCheck(undefined); @@ -105,7 +108,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { scope: undefined, severity: undefined, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', } as any; envHealthCheck .setup((e) => e.diagnose(typemoq.It.isAny())) @@ -149,7 +152,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, severity: DiagnosticSeverity.Error, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; diagnostics.push(diagnostic); } @@ -160,7 +163,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, severity: DiagnosticSeverity.Warning, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; diagnostics.push(diagnostic); } @@ -171,28 +174,11 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, severity: DiagnosticSeverity.Information, resource: undefined, - invokeHandler: 'default' + 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)) @@ -212,18 +198,20 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { 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 workspaceService = mock<IWorkspaceService>(); const foreGroundDeferred = createDeferred<IDiagnostic[]>(); const backgroundGroundDeferred = createDeferred<IDiagnostic[]>(); + when(svcContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(workspaceService); + when(workspaceService.isTrusted).thenReturn(true); when(svcContainer.getAll<IDiagnosticsService>(IDiagnosticsService)).thenReturn([ instance(foreGroundService), - instance(backGroundService) + instance(backGroundService), ]); when(foreGroundService.runInBackground).thenReturn(false); when(backGroundService.runInBackground).thenReturn(true); @@ -231,7 +219,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { when(foreGroundService.diagnose(anything())).thenReturn(foreGroundDeferred.promise); when(backGroundService.diagnose(anything())).thenReturn(backgroundGroundDeferred.promise); - const service = new ApplicationDiagnostics(instance(svcContainer), outputChannel.object); + const service = new ApplicationDiagnostics(instance(svcContainer)); const promise = service.performPreStartupHealthCheck(undefined); const deferred = createDeferredFromPromise(promise); @@ -240,11 +228,11 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { verify(foreGroundService.runInBackground).atLeast(1); verify(backGroundService.runInBackground).atLeast(1); - assert.equal(deferred.completed, false); + assert.strictEqual(deferred.completed, false); foreGroundDeferred.resolve([]); await sleep(1); - assert.equal(deferred.completed, true); + assert.strictEqual(deferred.completed, true); backgroundGroundDeferred.resolve([]); await sleep(1); diff --git a/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts index e0e37dd6c774..c6c4ff06ee74 100644 --- a/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts +++ b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts @@ -13,7 +13,7 @@ import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/ap import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; import { DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt + MessageCommandPrompt, } from '../../../../client/application/diagnostics/promptHandler'; import { DiagnosticScope, @@ -21,7 +21,7 @@ import { IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, - IDiagnosticsService + IDiagnosticsService, } from '../../../../client/application/diagnostics/types'; import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; @@ -29,7 +29,6 @@ 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>; @@ -54,8 +53,8 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { .setup((s) => s.get( typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) - ) + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), + ), ) .returns(() => messageHandler.object); @@ -181,9 +180,9 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', - options: DiagnosticScope.Global - }) - ) + options: DiagnosticScope.Global, + }), + ), ) .returns(() => alwaysIgnoreCommand.object) .verifiable(typemoq.Times.once()); @@ -192,8 +191,8 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { .setup((f) => f.createCommand( typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) - ) + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }), + ), ) .returns(() => launchBrowserCommand.object) .verifiable(typemoq.Times.once()); @@ -211,7 +210,7 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { filterService .setup((f) => - f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic)) + f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic)), ) .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); diff --git a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts deleted file mode 100644 index 3c27c5561935..000000000000 --- a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts +++ /dev/null @@ -1,523 +0,0 @@ -// 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 deleted file mode 100644 index 28e415be967e..000000000000 --- a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +++ /dev/null @@ -1,419 +0,0 @@ -// 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/jediPython27NotSupported.unit.test.ts b/src/test/application/diagnostics/checks/jediPython27NotSupported.unit.test.ts new file mode 100644 index 000000000000..d4af2e5ca901 --- /dev/null +++ b/src/test/application/diagnostics/checks/jediPython27NotSupported.unit.test.ts @@ -0,0 +1,510 @@ +// 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 { ConfigurationTarget, Uri } from 'vscode'; +import { LanguageServerType } from '../../../../client/activation/types'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + JediPython27NotSupportedDiagnostic, + JediPython27NotSupportedDiagnosticService, +} from '../../../../client/application/diagnostics/checks/jediPython27NotSupported'; +import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { + DiagnosticCommandPromptHandlerService, + MessageCommandPrompt, +} from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, +} from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; +import { Python27Support } from '../../../../client/common/utils/localize'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Jedi with Python 2.7 deprecated', () => { + suite('Diagnostics', () => { + const resource = Uri.file('test.py'); + + function createConfigurationAndWorkspaceServices( + languageServer: LanguageServerType, + ): { configurationService: IConfigurationService; workspaceService: IWorkspaceService } { + const configurationService = ({ + getSettings: () => ({ languageServer }), + updateSetting: () => Promise.resolve(), + } as unknown) as IConfigurationService; + + const workspaceService = ({ + getConfiguration: () => ({ + inspect: () => ({ + workspaceValue: languageServer, + }), + }), + } as unknown) as IWorkspaceService; + + return { configurationService, workspaceService }; + } + + test('Should return an empty diagnostics array if the active interpreter version is Python 3', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 3, + minor: 8, + patch: 0, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + + test('Should return an empty diagnostics array if the active interpreter is undefined', async () => { + const interpreterService = { + getActiveInterpreter: () => Promise.resolve(undefined), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + + test('Should return a diagnostics array with one diagnostic if the active interpreter version is Python 2.7', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + const diagnostic = result[0]; + + assert.strictEqual(result.length, 1); + assert.strictEqual(diagnostic.message, Python27Support.jediMessage); + }); + + test('Should return a diagnostics array with one diagnostic if the language server is Jedi', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + const diagnostic = result[0]; + + assert.strictEqual(result.length, 1); + assert.strictEqual(diagnostic.message, Python27Support.jediMessage); + }); + + test('Should return an empty diagnostics array if the language server is Pylance', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Node, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + + test('Should return an empty diagnostics array if there is no language server', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.None, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + }); + + suite('Setting update', () => { + const resource = Uri.file('test.py'); + let workspaceService: IWorkspaceService; + let getConfigurationStub: sinon.SinonStub; + let updateSettingStub: sinon.SinonStub; + let serviceContainer: IServiceContainer; + let services: { + [key: string]: IWorkspaceService; + }; + + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + setup(() => { + serviceContainer = ({ + get: (serviceIdentifier: symbol) => services[serviceIdentifier.toString()] as IWorkspaceService, + tryGet: () => ({}), + } as unknown) as IServiceContainer; + + workspaceService = new WorkspaceService(); + services = { + 'Symbol(IWorkspaceService)': workspaceService, + }; + + getConfigurationStub = sinon.stub(WorkspaceService.prototype, 'getConfiguration'); + updateSettingStub = sinon.stub(ConfigurationService.prototype, 'updateSetting'); + + const getSettingsStub = sinon.stub(ConfigurationService.prototype, 'getSettings'); + getSettingsStub.returns(({ + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as unknown) as IPythonSettings); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Running the diagnostic should update the workspace setting if set', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + workspaceValue: LanguageServerType.JediLSP, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.calledWith( + updateSettingStub, + 'languageServer', + LanguageServerType.Jedi, + resource, + ConfigurationTarget.Workspace, + ); + }); + + test('Running the diagnostic should update the global setting if set', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + globalValue: LanguageServerType.JediLSP, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.calledWith( + updateSettingStub, + 'languageServer', + LanguageServerType.Jedi, + resource, + ConfigurationTarget.Global, + ); + }); + + test('Running the diagnostic should not update the setting if not set in workspace or global scopes', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + workspaceFolderValue: LanguageServerType.JediLSP, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.notCalled(updateSettingStub); + }); + + test('Running the diagnostic should not update the setting if not set to Jedi LSP', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + workspaceValue: LanguageServerType.Node, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.notCalled(updateSettingStub); + }); + }); + + suite('Handler', () => { + class TestJediPython27NotSupportedDiagnosticService extends JediPython27NotSupportedDiagnosticService { + // eslint-disable-next-line class-methods-use-this + public static clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + } + + let services: { + [key: string]: IWorkspaceService | IDiagnosticFilterService | IDiagnosticsCommandFactory; + }; + let serviceContainer: IServiceContainer; + let handleMessageStub: sinon.SinonStub; + + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + setup(() => { + services = { + 'Symbol(IDiagnosticsCommandFactory)': { + createCommand: () => ({} as IDiagnosticCommand), + }, + }; + serviceContainer = { + get: (serviceIdentifier: symbol) => + services[serviceIdentifier.toString()] as IDiagnosticFilterService | IDiagnosticsCommandFactory, + } as IServiceContainer; + + handleMessageStub = sinon.stub(DiagnosticCommandPromptHandlerService.prototype, 'handle'); + }); + + teardown(() => { + sinon.restore(); + TestJediPython27NotSupportedDiagnosticService.clear(); + }); + + test('Handling an empty diagnostics array does not display a prompt', async () => { + const service = new TestJediPython27NotSupportedDiagnosticService( + serviceContainer, + interpreterService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.handle([]); + + sinon.assert.notCalled(handleMessageStub); + }); + + test('Handling a diagnostic that should be ignored does not display a prompt', async () => { + const diagnosticHandlerService = new DiagnosticCommandPromptHandlerService(serviceContainer); + + services['Symbol(IDiagnosticFilterService)'] = ({ + shouldIgnoreDiagnostic: async () => Promise.resolve(true), + } as unknown) as IDiagnosticFilterService; + + const service = new TestJediPython27NotSupportedDiagnosticService( + serviceContainer, + interpreterService, + {} as IWorkspaceService, + {} as IConfigurationService, + diagnosticHandlerService, + [], + ); + + await service.handle([new JediPython27NotSupportedDiagnostic('ignored', undefined)]); + + sinon.assert.notCalled(handleMessageStub); + }); + + test('Handling a diagnostic should show a prompt', async () => { + const diagnosticHandlerService = new DiagnosticCommandPromptHandlerService(serviceContainer); + const configurationService = new ConfigurationService(serviceContainer); + + services['Symbol(IDiagnosticFilterService)'] = ({ + shouldIgnoreDiagnostic: () => Promise.resolve(false), + } as unknown) as IDiagnosticFilterService; + + const service = new TestJediPython27NotSupportedDiagnosticService( + serviceContainer, + interpreterService, + {} as IWorkspaceService, + configurationService, + diagnosticHandlerService, + [], + ); + + const diagnostic = new JediPython27NotSupportedDiagnostic('diagnostic', undefined); + + await service.handle([diagnostic]); + + sinon.assert.calledOnce(handleMessageStub); + }); + }); +}); diff --git a/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts b/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts deleted file mode 100644 index 4f537b62c5bf..000000000000 --- a/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -// 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 index 1eb3d78b1ae7..ba2436d0ffeb 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -3,21 +3,18 @@ 'use strict'; -// tslint:disable:max-func-body-length no-any max-classes-per-file - -import { assert, expect } from 'chai'; +import { expect } from 'chai'; import * as typemoq from 'typemoq'; -import { ConfigurationChangeEvent, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; import { InvalidMacPythonInterpreterDiagnostic, - InvalidMacPythonInterpreterService + 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 + MessageCommandPrompt, } from '../../../../client/application/diagnostics/promptHandler'; import { DiagnosticScope, @@ -25,36 +22,32 @@ import { IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, - IDiagnosticsService + 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 { IInterpreterHelper } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; -import { InterpreterType } 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>; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; const pythonPath = 'My Python Path in Settings'; let serviceContainer: typemoq.IMock<IServiceContainer>; function createContainer() { @@ -64,8 +57,8 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { .setup((s) => s.get( typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) - ) + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), + ), ) .returns(() => messageHandler.object); commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); @@ -79,10 +72,6 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { 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))) @@ -95,6 +84,10 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) .returns(() => filterService.object); + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IInterpreterPathService))) + .returns(() => interpreterPathService.object); platformService .setup((p) => p.isMac) .returns(() => true) @@ -112,15 +105,12 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { protected addPythonPathChangedHandler() { noop(); } - })(createContainer(), interpreterService.object, [], platformService.object, helper.object); + })(createContainer(), [], platformService.object, helper.object); (diagnosticService as any)._clear(); }); test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { - for (const code of [ - DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic - ]) { + for (const code of [DiagnosticCodes.MacInterpreterSelected]) { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); diagnostic .setup((d) => d.code) @@ -154,175 +144,42 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { 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({ type: InterpreterType.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({ type: InterpreterType.Unknown } as any); - }) - .verifiable(typemoq.Times.once()); + test('Should return empty diagnostics if platform is mac and selected interpreter is not default mac interpreter', async () => { platformService .setup((i) => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isAny())) - .returns(() => false) + .returns(() => Promise.resolve(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({ type: InterpreterType.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()); + test('Should return diagnostic if platform is mac and selected interpreter is default mac interpreter', async () => { 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) + .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.atLeastOnce()); - interpreterService - .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { - return Promise.resolve({ type: InterpreterType.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' + [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelected, 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 + DiagnosticCodes.MacInterpreterSelected, + undefined, ); const cmd = ({} as any) as IDiagnosticCommand; const cmdIgnore = ({} as any) as IDiagnosticCommand; @@ -337,9 +194,9 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ - type: 'executeVSCCommand' - }) - ) + type: 'executeVSCCommand', + }), + ), ) .returns(() => cmd) .verifiable(typemoq.Times.once()); @@ -349,9 +206,9 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', - options: DiagnosticScope.Global - }) - ) + options: DiagnosticScope.Global, + }), + ), ) .returns(() => cmdIgnore) .verifiable(typemoq.Times.once()); @@ -363,83 +220,17 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { 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 } + { prompt: "Don't 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 + DiagnosticCodes.MacInterpreterSelected, + undefined, ); filterService - .setup((f) => - f.shouldIgnoreDiagnostic( - typemoq.It.isValue(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic) - ) - ) + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.MacInterpreterSelected))) .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); commandFactory @@ -464,66 +255,15 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { protected addPythonPathChangedHandler() { invoked = true; } - })(createContainer(), interpreterService.object, [], platformService.object, helper.object); + })(createContainer(), [], 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>(); + test('Diagnostics are checked with correct interpreter config uri when path changes', async () => { + const event = typemoq.Mock.ofType<InterpreterConfigurationScope>(); 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) @@ -531,41 +271,24 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { 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); + constructor(arg1: IServiceContainer, arg3: IPlatformService, arg4: IInterpreterHelper) { + super(arg1, [], arg3, arg4); this.changeThrottleTimeout = 1; } - public onDidChangeConfigurationEx = (e: ConfigurationChangeEvent) => super.onDidChangeConfiguration(e); + public onDidChangeConfigurationEx = (e: InterpreterConfigurationScope) => + 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 + 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); @@ -576,104 +299,11 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { 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 event = typemoq.Mock.ofType<InterpreterConfigurationScope>(); 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) @@ -681,85 +311,57 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { 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); + constructor(arg1: IServiceContainer, arg3: IPlatformService, arg4: IInterpreterHelper) { + super(arg1, [], arg3, arg4); this.changeThrottleTimeout = 100; } - public onDidChangeConfigurationEx = (e: ConfigurationChangeEvent) => super.onDidChangeConfiguration(e); + public onDidChangeConfigurationEx = (e: InterpreterConfigurationScope) => + 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 + 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 () => { + test('Ensure event Handler is registered correctly', async () => { let interpreterPathServiceHandler: Function; + let invoked = false; 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 - ); + + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + protected async onDidChangeConfiguration(_i: InterpreterConfigurationScope) { + invoked = true; + } + })(serviceContainerObject, [], undefined as any, undefined as any); expect(interpreterPathServiceHandler!).to.not.equal(undefined, 'Handler not set'); + await interpreterPathServiceHandler!({} as any); + expect(invoked).to.be.equal(true, 'Not invoked'); }); }); }); diff --git a/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts b/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts index fc5a1bb6a35e..29a6c6eb3aff 100644 --- a/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts +++ b/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts @@ -11,7 +11,7 @@ import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/ap import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; import { DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt + MessageCommandPrompt, } from '../../../../client/application/diagnostics/promptHandler'; import { DiagnosticScope, @@ -19,7 +19,7 @@ import { IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, - IDiagnosticsService + IDiagnosticsService, } from '../../../../client/application/diagnostics/types'; import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; @@ -27,7 +27,6 @@ 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>; @@ -52,8 +51,8 @@ suite('Application Diagnostics - PowerShell Activation', () => { .setup((s) => s.get( typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) - ) + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), + ), ) .returns(() => messageHandler.object); @@ -136,9 +135,9 @@ suite('Application Diagnostics - PowerShell Activation', () => { typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', - options: DiagnosticScope.Global - }) - ) + options: DiagnosticScope.Global, + }), + ), ) .returns(() => alwaysIgnoreCommand.object) .verifiable(typemoq.Times.once()); @@ -147,8 +146,8 @@ suite('Application Diagnostics - PowerShell Activation', () => { .setup((f) => f.createCommand( typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) - ) + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }), + ), ) .returns(() => launchBrowserCommand.object) .verifiable(typemoq.Times.once()); diff --git a/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts b/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts new file mode 100644 index 000000000000..85dc5a4fb8af --- /dev/null +++ b/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts @@ -0,0 +1,158 @@ +// 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 typemoq from 'typemoq'; +import { ExtensionContext } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + PylanceDefaultDiagnostic, + PylanceDefaultDiagnosticService, + PYLANCE_PROMPT_MEMENTO, +} from '../../../../client/application/diagnostics/checks/pylanceDefault'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnostic, + IDiagnosticFilterService, + IDiagnosticHandlerService, +} from '../../../../client/application/diagnostics/types'; +import { IExtensionContext } from '../../../../client/common/types'; +import { Common, Diagnostics } from '../../../../client/common/utils/localize'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Pylance informational prompt', () => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let diagnosticService: PylanceDefaultDiagnosticService; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let context: typemoq.IMock<IExtensionContext>; + let memento: typemoq.IMock<ExtensionContext['globalState']>; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + context = typemoq.Mock.ofType<IExtensionContext>(); + memento = typemoq.Mock.ofType<ExtensionContext['globalState']>(); + + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + context.setup((c) => c.globalState).returns(() => memento.object); + + diagnosticService = new (class extends PylanceDefaultDiagnosticService { + // eslint-disable-next-line class-methods-use-this + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, context.object, messageHandler.object, []); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (diagnosticService as any)._clear(); + }); + + teardown(() => { + context.reset(); + memento.reset(); + }); + + function setupMementos(version?: string, promptShown?: boolean) { + diagnosticService.initialMementoValue = version; + memento.setup((m) => m.get(PYLANCE_PROMPT_MEMENTO)).returns(() => promptShown); + } + + test("Should display message if it's an existing installation of the extension and the prompt has not been shown yet", async () => { + setupMementos('1.0.0', undefined); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, [ + new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage, undefined), + ]); + }); + + test("Should return empty diagnostics if it's an existing installation of the extension and the prompt has been shown before", async () => { + setupMementos('1.0.0', true); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, []); + }); + + test("Should return empty diagnostics if it's a fresh installation of the extension", async () => { + setupMementos(undefined, undefined); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, []); + }); + + test('Should display a prompt when handling the diagnostic code', async () => { + const diagnostic = new PylanceDefaultDiagnostic(DiagnosticCodes.PylanceDefaultDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + + assert.notDeepStrictEqual(messagePrompt, undefined); + assert.notDeepStrictEqual(messagePrompt!.onClose, undefined); + assert.deepStrictEqual(messagePrompt!.commandPrompts, [{ prompt: Common.ok }]); + }); + + test('Should return empty diagnostics if the diagnostic code has been ignored', async () => { + const diagnostic = new PylanceDefaultDiagnostic(DiagnosticCodes.PylanceDefaultDiagnostic, undefined); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.PylanceDefaultDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + messageHandler.setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + }); + + test('PylanceDefaultDiagnosticService can handle PylanceDefaultDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.PylanceDefaultDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('PylanceDefaultDiagnosticService cannot handle non-PylanceDefaultDiagnostic 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(false, 'Invalid value'); + diagnostic.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index 863f87cf53ca..2eecf052e433 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -3,68 +3,95 @@ 'use strict'; -// tslint:disable:max-func-body-length no-any max-classes-per-file - import { expect } from 'chai'; import * as typemoq from 'typemoq'; +import { EventEmitter, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidLaunchJsonDebuggerDiagnostic } from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; import { + DefaultShellDiagnostic, InvalidPythonInterpreterDiagnostic, - InvalidPythonInterpreterService + 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 + MessageCommandPrompt, } from '../../../../client/application/diagnostics/promptHandler'; import { + DiagnosticScope, 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 { ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { + IConfigurationService, + IDisposable, + IDisposableRegistry, + IInterpreterPathService, + Resource, +} from '../../../../client/common/types'; +import { Common } from '../../../../client/common/utils/localize'; import { noop } from '../../../../client/common/utils/misc'; -import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; -import { InterpreterType, PythonInterpreter } from '../../../../client/pythonEnvironments/info'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { getOSType, OSType } from '../../../common'; +import { sleep } from '../../../core'; suite('Application Diagnostics - Checks Python Interpreter', () => { - let diagnosticService: IDiagnosticsService; + let diagnosticService: InvalidPythonInterpreterService; 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 workspaceService: typemoq.IMock<IWorkspaceService>; + let commandManager: typemoq.IMock<ICommandManager>; + let configService: typemoq.IMock<IConfigurationService>; + let fs: typemoq.IMock<IFileSystem>; let serviceContainer: typemoq.IMock<IServiceContainer>; + let processService: typemoq.IMock<IProcessService>; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; + const oldComSpec = process.env.ComSpec; + const oldPath = process.env.Path; function createContainer() { + fs = typemoq.Mock.ofType<IFileSystem>(); + fs.setup((f) => f.fileExists(process.env.ComSpec ?? 'exists')).returns(() => Promise.resolve(true)); serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + processService = typemoq.Mock.ofType<IProcessService>(); + const processServiceFactory = typemoq.Mock.ofType<IProcessServiceFactory>(); + processServiceFactory.setup((p) => p.create()).returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((p) => (p as any).then).returns(() => undefined); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + commandManager = typemoq.Mock.ofType<ICommandManager>(); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IFileSystem))).returns(() => fs.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(ICommandManager))).returns(() => commandManager.object); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); serviceContainer .setup((s) => s.get( typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) - ) + 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))) @@ -73,8 +100,16 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { 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); + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + interpreterPathService.setup((i) => i.get(typemoq.It.isAny())).returns(() => 'customPython'); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IInterpreterPathService))) + .returns(() => interpreterPathService.object); + configService = typemoq.Mock.ofType<IConfigurationService>(); + configService.setup((c) => c.getSettings()).returns(() => ({ pythonPath: 'pythonPath' } as any)); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); return serviceContainer.object; } @@ -93,10 +128,57 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { (diagnosticService as any)._clear(); }); + teardown(() => { + process.env.ComSpec = oldComSpec; + process.env.Path = oldPath; + }); + + test('Registers command to trigger environment prompts', async () => { + let triggerFunction: ((resource: Resource) => Promise<boolean>) | undefined; + commandManager + .setup((c) => c.registerCommand(Commands.TriggerEnvironmentSelection, typemoq.It.isAny())) + .callback((_, cb) => (triggerFunction = cb)) + .returns(() => typemoq.Mock.ofType<IDisposable>().object); + await diagnosticService.activate(); + expect(triggerFunction).to.not.equal(undefined); + interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(false)); + let result1 = await triggerFunction!(undefined); + expect(result1).to.equal(false); + + interpreterService.reset(); + interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'interpreterpath' } as unknown) as PythonEnvironment)); + const result2 = await triggerFunction!(undefined); + expect(result2).to.equal(true); + }); + + test('Changes to interpreter configuration triggers environment prompts', async () => { + commandManager + .setup((c) => c.registerCommand(Commands.TriggerEnvironmentSelection, typemoq.It.isAny())) + .returns(() => typemoq.Mock.ofType<IDisposable>().object); + const interpreterEvent = new EventEmitter<Uri | undefined>(); + interpreterService + .setup((i) => i.onDidChangeInterpreterConfiguration) + .returns(() => interpreterEvent.event); + await diagnosticService.activate(); + + commandManager + .setup((c) => c.executeCommand(Commands.TriggerEnvironmentSelection, undefined)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + interpreterEvent.fire(undefined); + await sleep(1); + + commandManager.verifyAll(); + }); + test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { for (const code of [ DiagnosticCodes.NoPythonInterpretersDiagnostic, - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, ]) { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); diagnostic @@ -109,132 +191,164 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { 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()); + test('Should return empty diagnostics', async () => { const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); + expect(diagnostics).to.be.deep.equal([], 'not the same'); }); - test('Should return diagnostics if there are no interpreters after double-checking', async () => { - settings - .setup((s) => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); + + test('Should return diagnostics if there are no interpreters and no interpreter has been explicitly set', async () => { + interpreterPathService.reset(); + interpreterPathService.setup((i) => i.get(typemoq.It.isAny())).returns(() => 'python'); interpreterService - .setup((i) => i.hasInterpreters) + .setup((i) => i.hasInterpreters()) .returns(() => Promise.resolve(false)) .verifiable(typemoq.Times.once()); interpreterService .setup((i) => i.getInterpreters(undefined)) - .returns(() => Promise.resolve([])) + .returns(() => []) .verifiable(typemoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); + const diagnostics = await diagnosticService._manualDiagnose(undefined); expect(diagnostics).to.be.deep.equal( - [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic, undefined)], - 'not the same' + [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + undefined, + workspaceService.object, + DiagnosticScope.Global, + ), + ], + 'not the same', ); - settings.verifyAll(); - interpreterService.verifyAll(); }); - test('Should return empty diagnostics if there are interpreters after double-checking', async () => { - const interpreter: PythonInterpreter = { type: InterpreterType.Unknown } as any; + test('Should return comspec diagnostics if comspec is configured incorrectly', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + // No interpreter should exist if comspec is incorrectly configured. + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + // Should fail with this error code if comspec is incorrectly configured. + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + // Should be set to an invalid value in this case. + process.env.ComSpec = 'doesNotExist'; + fs.setup((f) => f.fileExists('doesNotExist')).returns(() => Promise.resolve(false)); - settings - .setup((s) => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, undefined)], + 'not the same', + ); + }); + test('Should return incomplete path diagnostics if `Path` variable is incomplete and execution fails', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + // No interpreter should exist if execution is failing. interpreterService - .setup((i) => i.hasInterpreters) - .returns(() => Promise.resolve(false)) - .verifiable(typemoq.Times.once()); + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + process.env.Path = 'SystemRootDoesNotExist'; + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.IncompletePathVarDiagnostic, undefined)], + 'not the same', + ); + }); + test('Should return default shell error diagnostic if execution fails but we do not identify the cause', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + // No interpreter should exist if execution is failing. interpreterService - .setup((i) => i.getInterpreters(undefined)) - .returns(() => Promise.resolve([interpreter])) - .verifiable(typemoq.Times.once()); + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + process.env.Path = 'C:\\Windows\\System32'; + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, undefined)], + 'not the same', + ); + }); + test('Should return invalid interpreter diagnostics on non-Windows if there is no current interpreter and execution fails', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(false)); + // No interpreter should exist if execution is failing. 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(); + return Promise.resolve(undefined); + }); + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + undefined, + workspaceService.object, + ), + ], + 'not the same', + ); }); - 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()); + test('Should return invalid interpreter diagnostics if there are interpreters but no current interpreter', async () => { interpreterService - .setup((i) => i.hasInterpreters) + .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); + const diagnostics = await diagnosticService._manualDiagnose(undefined); expect(diagnostics).to.be.deep.equal( [ new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - undefined - ) + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + undefined, + workspaceService.object, + ), ], - 'not the same' + '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.hasInterpreters()).returns(() => Promise.resolve(true)); interpreterService .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { - return Promise.resolve({ type: InterpreterType.Unknown } as any); - }) - .verifiable(typemoq.Times.once()); + return Promise.resolve({ envType: EnvironmentType.Unknown } as any); + }); - const diagnostics = await diagnosticService.diagnose(undefined); + const diagnostics = await diagnosticService._manualDiagnose(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 - ); + + test('Handling comspec diagnostic should launch expected browser link', async () => { + const diagnostic = new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, undefined); const cmd = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; messageHandler @@ -246,8 +360,11 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .setup((f) => f.createCommand( typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) - ) + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ + type: 'launch', + options: 'https://aka.ms/AAk3djo', + }), + ), ) .returns(() => cmd) .verifiable(typemoq.Times.once()); @@ -257,14 +374,16 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { 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.'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { + prompt: Common.seeInstructions, + command: cmd, + }, + ]); }); - test('Handling no currently selected interpreter diagnostic should show select interpreter message', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - undefined - ); + + test('Handling incomplete path diagnostic should launch expected browser link', async () => { + const diagnostic = new DefaultShellDiagnostic(DiagnosticCodes.IncompletePathVarDiagnostic, undefined); const cmd = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; messageHandler @@ -276,10 +395,46 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .setup((f) => f.createCommand( typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ - type: 'executeVSCCommand' - }) - ) + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ + type: 'launch', + options: 'https://aka.ms/AAk744c', + }), + ), + ) + .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: Common.seeInstructions, + command: cmd, + }, + ]); + }); + + test('Handling default shell error diagnostic should launch expected browser link', async () => { + const diagnostic = new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, 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', + options: 'https://aka.ms/AAk7qix', + }), + ), ) .returns(() => cmd) .verifiable(typemoq.Times.once()); @@ -289,15 +444,19 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { 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 } + { + prompt: Common.seeInstructions, + command: cmd, + }, ]); }); + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { const diagnostic = new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - undefined + DiagnosticCodes.NoPythonInterpretersDiagnostic, + undefined, + workspaceService.object, ); const cmd = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; @@ -311,9 +470,10 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ - type: 'executeVSCCommand' - }) - ) + type: 'executeVSCCommand', + options: Commands.Set_Interpreter, + }), + ), ) .returns(() => cmd) .verifiable(typemoq.Times.once()); @@ -323,48 +483,55 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { 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 } + { + prompt: Common.selectPythonInterpreter, + command: cmd, + }, ]); + expect(messagePrompt!.onClose).to.not.be.equal(undefined, 'onClose handler should be set.'); }); - test('Handling an empty diagnostic should not show a message nor return a command', async () => { - const diagnostics: IDiagnostic[] = []; - const cmd = ({} as any) as IDiagnosticCommand; + test('Handling no currently selected interpreter diagnostic should show select interpreter message', async () => { + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + undefined, + workspaceService.object, + ); + const cmd = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; messageHandler - .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => p) + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.never()); + .verifiable(typemoq.Times.once()); commandFactory .setup((f) => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ - type: 'executeVSCCommand' - }) - ) + type: 'executeVSCCommand', + }), + ), ) .returns(() => cmd) - .verifiable(typemoq.Times.never()); + .verifiable(typemoq.Times.exactly(2)); - await diagnosticService.handle(diagnostics); + 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: Common.selectPythonInterpreter, command: cmd }, + { prompt: Common.openOutputPanel, command: cmd }, + ]); + expect(messagePrompt!.onClose).be.equal(undefined, 'onClose handler should not be set.'); }); - test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - undefined - ); + test('Handling an empty diagnostic should not show a message nor return a command', async () => { + const diagnostics: IDiagnostic[] = []; 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) @@ -375,22 +542,30 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ - type: 'executeVSCCommand' - }) - ) + type: 'executeVSCCommand', + }), + ), ) .returns(() => cmd) .verifiable(typemoq.Times.never()); - await diagnosticServiceMock.object.handle([diagnostic]); + await diagnosticService.handle(diagnostics); 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); + test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + undefined, + workspaceService.object, + ); 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) @@ -401,21 +576,14 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ - type: 'executeVSCCommand' - }) - ) + 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' - ); - } + await diagnosticServiceMock.object.handle([diagnostic]); 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 deleted file mode 100644 index 7fab9fb2de27..000000000000 --- a/src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts +++ /dev/null @@ -1,394 +0,0 @@ -// 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/switchToDefaultLSDiagnostic.unit.test.ts b/src/test/application/diagnostics/checks/switchToDefaultLSDiagnostic.unit.test.ts new file mode 100644 index 000000000000..c3d1c9e18fec --- /dev/null +++ b/src/test/application/diagnostics/checks/switchToDefaultLSDiagnostic.unit.test.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { SwitchToDefaultLanguageServerDiagnosticService } from '../../../../client/application/diagnostics/checks/switchToDefaultLS'; +import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { IDiagnosticFilterService, IDiagnosticHandlerService } from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { MockWorkspaceConfiguration } from '../../../mocks/mockWorkspaceConfig'; + +suite('Application Diagnostics - Switch to default LS', () => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let diagnosticService: SwitchToDefaultLanguageServerDiagnosticService; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + + diagnosticService = new SwitchToDefaultLanguageServerDiagnosticService( + serviceContainer.object, + workspaceService.object, + messageHandler.object, + [], + ); + }); + + test('When global language server is NOT Microsoft do Nothing', async () => { + workspaceService + .setup((w) => w.getConfiguration('python')) + .returns( + () => + new MockWorkspaceConfiguration({ + languageServer: { + globalValue: 'Default', + workspaceValue: undefined, + }, + }), + ); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(0, 'Diagnostics should not be returned for this case'); + }); + test('When global language server is Microsoft', async () => { + const config = new MockWorkspaceConfiguration({ + languageServer: { + globalValue: 'Microsoft', + workspaceValue: undefined, + }, + }); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => config); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(1, 'Diagnostics should be returned for this case'); + const value = config.inspect<string>('languageServer'); + expect(value).to.be.equals('Default', 'Global language server value should be Default'); + }); + + test('When workspace language server is NOT Microsoft do Nothing', async () => { + workspaceService + .setup((w) => w.getConfiguration('python')) + .returns( + () => + new MockWorkspaceConfiguration({ + languageServer: { + globalValue: undefined, + workspaceValue: 'Default', + }, + }), + ); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(0, 'Diagnostics should not be returned for this case'); + }); + test('When workspace language server is Microsoft', async () => { + const config = new MockWorkspaceConfiguration({ + languageServer: { + globalValue: undefined, + workspaceValue: 'Microsoft', + }, + }); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => config); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(1, 'Diagnostics should be returned for this case'); + const value = config.inspect<string>('languageServer'); + expect(value).to.be.equals('Default', 'Workspace language server value should be Default'); + }); +}); diff --git a/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts b/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts deleted file mode 100644 index 039086a404a3..000000000000 --- a/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -// 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 deleted file mode 100644 index e0f220f41080..000000000000 --- a/src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts +++ /dev/null @@ -1,346 +0,0 @@ -// 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 index 56f9e51e0051..24881c71833b 100644 --- a/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts +++ b/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts @@ -29,7 +29,7 @@ suite('Application Diagnostics - Exec VSC Commands', () => { const command = commandFactory.createCommand(diagnostic.object, { type: 'executeVSCCommand', - options: 'editor.action.formatDocument' + options: 'editor.action.formatDocument', }); expect(command).to.be.instanceOf(ExecuteVSCCommand); }); @@ -43,7 +43,7 @@ suite('Application Diagnostics - Exec VSC Commands', () => { const command = commandFactory.createCommand(diagnostic.object, { type: 'executeVSCCommand', - options: 'editor.action.formatDocument' + options: 'editor.action.formatDocument', }); await command.invoke(); diff --git a/src/test/application/diagnostics/commands/factory.unit.test.ts b/src/test/application/diagnostics/commands/factory.unit.test.ts index a8e534408ad6..82db96aa3dec 100644 --- a/src/test/application/diagnostics/commands/factory.unit.test.ts +++ b/src/test/application/diagnostics/commands/factory.unit.test.ts @@ -24,7 +24,7 @@ suite('Application Diagnostics - Commands Factory', () => { const command = commandFactory.createCommand(diagnostic.object, { type: 'ignore', - options: DiagnosticScope.Global + options: DiagnosticScope.Global, }); expect(command).to.be.instanceOf(IgnoreDiagnosticCommand); }); diff --git a/src/test/application/diagnostics/commands/ignore.unit.test.ts b/src/test/application/diagnostics/commands/ignore.unit.test.ts index 65eb73c16d12..90c8e38f8470 100644 --- a/src/test/application/diagnostics/commands/ignore.unit.test.ts +++ b/src/test/application/diagnostics/commands/ignore.unit.test.ts @@ -9,12 +9,10 @@ import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, - IDiagnosticFilterService + 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>; diff --git a/src/test/application/diagnostics/filter.unit.test.ts b/src/test/application/diagnostics/filter.unit.test.ts index fbd23e7390da..996f4e59a52b 100644 --- a/src/test/application/diagnostics/filter.unit.test.ts +++ b/src/test/application/diagnostics/filter.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -22,8 +20,8 @@ suite('Application Diagnostics - Filter', () => { name: 'Workspace', scope: DiagnosticScope.WorkspaceFolder, state: () => workspaceState, - otherState: () => globalState - } + otherState: () => globalState, + }, ].forEach((item) => { let serviceContainer: typemoq.IMock<IServiceContainer>; let filterService: IDiagnosticFilterService; @@ -39,16 +37,16 @@ suite('Application Diagnostics - Filter', () => { .setup((f) => f.createGlobalPersistentState<string[]>( typemoq.It.isValue(FilterKeys.GlobalDiagnosticFilter), - typemoq.It.isValue([]) - ) + typemoq.It.isValue([]), + ), ) .returns(() => globalState.object); stateFactory .setup((f) => f.createWorkspacePersistentState<string[]>( typemoq.It.isValue(FilterKeys.WorkspaceDiagnosticFilter), - typemoq.It.isValue([]) - ) + typemoq.It.isValue([]), + ), ) .returns(() => workspaceState.object); serviceContainer diff --git a/src/test/application/diagnostics/promptHandler.unit.test.ts b/src/test/application/diagnostics/promptHandler.unit.test.ts index 0f9b33017a73..0c8d732b15f4 100644 --- a/src/test/application/diagnostics/promptHandler.unit.test.ts +++ b/src/test/application/diagnostics/promptHandler.unit.test.ts @@ -3,20 +3,18 @@ '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 + MessageCommandPrompt, } from '../../../client/application/diagnostics/promptHandler'; import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, - IDiagnosticHandlerService + IDiagnosticHandlerService, } from '../../../client/application/diagnostics/types'; import { IApplicationShell } from '../../../client/common/application/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; @@ -44,7 +42,7 @@ suite('Application Diagnostics - PromptHandler', () => { scope: DiagnosticScope.Global, severity: severity.value, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; switch (severity.value) { case DiagnosticSeverity.Error: { @@ -77,7 +75,7 @@ suite('Application Diagnostics - PromptHandler', () => { scope: DiagnosticScope.Global, severity: severity.value, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; let onCloseHandlerInvoked = false; const options: MessageCommandPrompt = { @@ -85,7 +83,7 @@ suite('Application Diagnostics - PromptHandler', () => { message: 'Custom Message', onClose: () => { onCloseHandlerInvoked = true; - } + }, }; switch (severity.value) { @@ -95,8 +93,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showErrorMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); @@ -108,8 +106,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showWarningMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); @@ -121,8 +119,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showInformationMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); @@ -141,11 +139,11 @@ suite('Application Diagnostics - PromptHandler', () => { scope: DiagnosticScope.Global, severity: severity.value, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; const options: MessageCommandPrompt = { commandPrompts: [{ prompt: 'Yes' }, { prompt: 'No' }], - message: 'Custom Message' + message: 'Custom Message', }; switch (severity.value) { @@ -155,8 +153,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showErrorMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .verifiable(typemoq.Times.once()); break; @@ -167,8 +165,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showWarningMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .verifiable(typemoq.Times.once()); break; @@ -179,8 +177,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showInformationMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .verifiable(typemoq.Times.once()); break; @@ -197,15 +195,15 @@ suite('Application Diagnostics - PromptHandler', () => { scope: DiagnosticScope.Global, severity: severity.value, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; const command = typemoq.Mock.ofType<IDiagnosticCommand>(); const options: MessageCommandPrompt = { commandPrompts: [ { prompt: 'Yes', command: command.object }, - { prompt: 'No', command: command.object } + { prompt: 'No', command: command.object }, ], - message: 'Custom Message' + message: 'Custom Message', }; command.setup((c) => c.invoke()).verifiable(typemoq.Times.once()); @@ -216,8 +214,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showErrorMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); @@ -229,8 +227,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showWarningMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); @@ -242,8 +240,8 @@ suite('Application Diagnostics - PromptHandler', () => { a.showInformationMessage( typemoq.It.isValue(options.message!), typemoq.It.isValue('Yes'), - typemoq.It.isValue('No') - ) + typemoq.It.isValue('No'), + ), ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); diff --git a/src/test/application/diagnostics/serviceRegistry.unit.test.ts b/src/test/application/diagnostics/serviceRegistry.unit.test.ts index 6c688abf7317..dcff47b2b7e7 100644 --- a/src/test/application/diagnostics/serviceRegistry.unit.test.ts +++ b/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -4,57 +4,49 @@ 'use strict'; import { instance, mock, verify } from 'ts-mockito'; -import { LanguageServerType } from '../../../client/activation/types'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; import { EnvironmentPathVariableDiagnosticsService, - EnvironmentPathVariableDiagnosticsServiceId + EnvironmentPathVariableDiagnosticsServiceId, } from '../../../client/application/diagnostics/checks/envPathVariable'; import { InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId + 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'; + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, +} from '../../../client/application/diagnostics/checks/jediPython27NotSupported'; import { InvalidMacPythonInterpreterService, - InvalidMacPythonInterpreterServiceId + InvalidMacPythonInterpreterServiceId, } from '../../../client/application/diagnostics/checks/macPythonInterpreter'; import { PowerShellActivationHackDiagnosticsService, - PowerShellActivationHackDiagnosticsServiceId + PowerShellActivationHackDiagnosticsServiceId, } from '../../../client/application/diagnostics/checks/powerShellActivation'; import { InvalidPythonInterpreterService, - InvalidPythonInterpreterServiceId + 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'; + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, +} from '../../../client/application/diagnostics/checks/switchToDefaultLS'; 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 + MessageCommandPrompt, } from '../../../client/application/diagnostics/promptHandler'; import { registerTypes } from '../../../client/application/diagnostics/serviceRegistry'; import { IDiagnosticFilterService, IDiagnosticHandlerService, - IDiagnosticsService + IDiagnosticsService, } from '../../../client/application/diagnostics/types'; import { IApplicationDiagnostics } from '../../../client/application/types'; import { ServiceManager } from '../../../client/ioc/serviceManager'; @@ -66,86 +58,85 @@ suite('Application Diagnostics - Register classes in IOC Container', () => { serviceManager = mock(ServiceManager); }); test('Register Classes', () => { - registerTypes(instance(serviceManager), LanguageServerType.Microsoft); + registerTypes(instance(serviceManager)); verify( - serviceManager.addSingleton<IDiagnosticFilterService>(IDiagnosticFilterService, DiagnosticFilterService) + serviceManager.addSingleton<IDiagnosticFilterService>(IDiagnosticFilterService, DiagnosticFilterService), ); verify( serviceManager.addSingleton<IDiagnosticHandlerService<MessageCommandPrompt>>( IDiagnosticHandlerService, DiagnosticCommandPromptHandlerService, - DiagnosticCommandPromptHandlerServiceId - ) + DiagnosticCommandPromptHandlerServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, EnvironmentPathVariableDiagnosticsService, - EnvironmentPathVariableDiagnosticsServiceId - ) + EnvironmentPathVariableDiagnosticsServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId - ) + InvalidLaunchJsonDebuggerServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, - UpgradeCodeRunnerDiagnosticService, - UpgradeCodeRunnerDiagnosticServiceId - ) + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, InvalidPythonInterpreterService, - InvalidPythonInterpreterServiceId - ) + InvalidPythonInterpreterServiceId, + ), ); verify( - serviceManager.addSingleton<IDiagnosticsService>( - IDiagnosticsService, - InvalidPythonPathInDebuggerService, - InvalidPythonPathInDebuggerServiceId - ) + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InvalidPythonInterpreterService, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, - LSNotSupportedDiagnosticService, - LSNotSupportedDiagnosticServiceId - ) + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, PowerShellActivationHackDiagnosticsService, - PowerShellActivationHackDiagnosticsServiceId - ) + PowerShellActivationHackDiagnosticsServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, InvalidMacPythonInterpreterService, - InvalidMacPythonInterpreterServiceId - ) + InvalidMacPythonInterpreterServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsService>( IDiagnosticsService, - PythonPathDeprecatedDiagnosticService, - PythonPathDeprecatedDiagnosticServiceId - ) + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, + ), ); verify( serviceManager.addSingleton<IDiagnosticsCommandFactory>( IDiagnosticsCommandFactory, - DiagnosticsCommandFactory - ) + DiagnosticsCommandFactory, + ), ); verify(serviceManager.addSingleton<IApplicationDiagnostics>(IApplicationDiagnostics, ApplicationDiagnostics)); }); diff --git a/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts b/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts deleted file mode 100644 index 4556b63d38e7..000000000000 --- a/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -// 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/chat/utils.unit.test.ts b/src/test/chat/utils.unit.test.ts new file mode 100644 index 000000000000..8d45c1ac118f --- /dev/null +++ b/src/test/chat/utils.unit.test.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { resolveFilePath } from '../../client/chat/utils'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Chat Utils - resolveFilePath()', () => { + let getWorkspaceFoldersStub: sinon.SinonStub; + + setup(() => { + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([]); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('When filepath is undefined or empty', () => { + test('Should return first workspace folder URI when workspace folders exist', () => { + const expectedUri = Uri.file('/test/workspace'); + const mockFolder: WorkspaceFolder = { + uri: expectedUri, + name: 'test', + index: 0, + }; + getWorkspaceFoldersStub.returns([mockFolder]); + + const result = resolveFilePath(undefined); + + expect(result?.toString()).to.equal(expectedUri.toString()); + }); + + test('Should return first folder when multiple workspace folders exist', () => { + const firstUri = Uri.file('/first/workspace'); + const secondUri = Uri.file('/second/workspace'); + const mockFolders: WorkspaceFolder[] = [ + { uri: firstUri, name: 'first', index: 0 }, + { uri: secondUri, name: 'second', index: 1 }, + ]; + getWorkspaceFoldersStub.returns(mockFolders); + + const result = resolveFilePath(undefined); + + expect(result?.toString()).to.equal(firstUri.toString()); + }); + + test('Should return undefined when no workspace folders exist', () => { + getWorkspaceFoldersStub.returns(undefined); + + const result = resolveFilePath(undefined); + + expect(result).to.be.undefined; + }); + + test('Should return undefined when workspace folders is empty array', () => { + getWorkspaceFoldersStub.returns([]); + + const result = resolveFilePath(undefined); + + expect(result).to.be.undefined; + }); + + test('Should return undefined for empty string when no workspace folders', () => { + getWorkspaceFoldersStub.returns(undefined); + + const result = resolveFilePath(''); + + expect(result).to.be.undefined; + }); + }); + + suite('Windows file paths', () => { + test('Should handle Windows path with lowercase drive letter', () => { + const filepath = 'c:\\GIT\\tests\\simple-python-app'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + // Uri.file normalizes drive letters to lowercase + expect(result?.fsPath.toLowerCase()).to.include('git'); + }); + + test('Should handle Windows path with uppercase drive letter', () => { + const filepath = 'C:\\Users\\test\\project'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.fsPath.toLowerCase()).to.include('users'); + }); + + test('Should handle Windows path with forward slashes', () => { + const filepath = 'C:/Users/test/project'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('Unix file paths', () => { + test('Should handle Unix absolute path', () => { + const filepath = '/home/user/projects/myapp'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.path).to.include('/home/user/projects/myapp'); + }); + + test('Should handle Unix root path', () => { + const filepath = '/'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('Relative paths', () => { + test('Should handle relative path with dot prefix', () => { + const filepath = './src/main.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle relative path without prefix', () => { + const filepath = 'src/main.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle parent directory reference', () => { + const filepath = '../other-project/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('URI schemes', () => { + test('Should handle file:// URI scheme', () => { + const filepath = 'file:///home/user/test.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.path).to.include('/home/user/test.py'); + }); + + test('Should handle vscode-notebook:// URI scheme', () => { + const filepath = 'vscode-notebook://jupyter/notebook.ipynb'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('vscode-notebook'); + }); + + test('Should handle untitled: URI scheme without double slash as file path', () => { + const filepath = 'untitled:Untitled-1'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + // untitled: doesn't have ://, so it will be treated as a file path + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle https:// URI scheme', () => { + const filepath = 'https://example.com/path'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('https'); + }); + + test('Should handle vscode-vfs:// URI scheme', () => { + const filepath = 'vscode-vfs://github/microsoft/vscode/file.ts'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('vscode-vfs'); + }); + }); + + suite('Edge cases', () => { + test('Should handle path with spaces', () => { + const filepath = '/home/user/my project/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle path with special characters', () => { + const filepath = '/home/user/project-name_v2/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should not treat Windows drive letter colon as URI scheme', () => { + // Windows path should not be confused with a URI scheme + const filepath = 'd:\\projects\\test'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should not treat single colon as URI scheme', () => { + // A path with a colon but not :// should be treated as a file + const filepath = 'c:somepath'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); +}); diff --git a/src/test/common.ts b/src/test/common.ts index 7c6dd4a70c4e..886323e815a5 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -2,19 +2,19 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-console no-require-imports no-var-requires +// IMPORTANT: Do not import anything from the 'client' folder in this file as that folder is not available during smoke tests. import * as assert from 'assert'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; 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 type { PythonExtension } from '../client/api/types'; import { IProcessService } from '../client/common/process/types'; -import { IDisposable, IPythonSettings, Resource } from '../client/common/types'; +import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; -import { PythonInterpreter } from '../client/pythonEnvironments/info'; +import { ProposedExtensionAPI } from '../client/proposedApiTypes'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_MULTI_ROOT_TEST, IS_PERF_TEST, IS_SMOKE_TEST } from './constants'; import { noop, sleep } from './core'; @@ -22,9 +22,7 @@ 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'); +const fileInNonRootWorkspace = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'python_files', 'dummy.py'); export const rootWorkspaceUri = getWorkspaceRoot(); export const PYTHON_PATH = getPythonPath(); @@ -36,38 +34,23 @@ export enum OSType { Unknown = 'Unknown', Windows = 'Windows', OSX = 'OSX', - Linux = 'Linux' + Linux = 'Linux', } export type PythonSettingKeys = - | 'workspaceSymbols.enabled' - | 'pythonPath' + | 'defaultInterpreterPath' | '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'); + const configSettings = await import('../client/common/configSettings.js'); configSettings.PythonSettings.dispose(); } } @@ -76,17 +59,11 @@ export async function updateSetting( setting: PythonSettingKeys, value: {} | undefined, resource: Uri | undefined, - configTarget: ConfigurationTarget + configTarget: ConfigurationTarget, ) { - // The hooks and tests which use this method often timeout especially in case of multiroot workspaces. - // I am suspecting this is because the configTarget is ConfigurationTarget.WorkspaceFolder - // and updates in that configTarget take longer time. Logging to see if there's any truth to this. - console.log(`Starting diagnosis for configuration: ${configTarget}`); - console.time('Update setting diagnosis'); const vscode = require('vscode') as typeof import('vscode'); - const settings = vscode.workspace.getConfiguration('python', resource || null); + const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' }); const currentValue = settings.inspect(setting); - console.timeLog('Update setting diagnosis'); if ( currentValue !== undefined && ((configTarget === vscode.ConfigurationTarget.Global && currentValue.globalValue === value) || @@ -95,12 +72,9 @@ export async function updateSetting( currentValue.workspaceFolderValue === value)) ) { await disposePythonSettings(); - console.timeEnd('Update setting diagnosis'); return; } - console.timeLog('Update setting diagnosis'); await settings.update(setting, value, configTarget); - console.timeLog('Update setting diagnosis'); // 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 @@ -110,8 +84,6 @@ export async function updateSetting( // ... please see issue #2356 and PR #2332 for a discussion on the matter await disposePythonSettings(); - console.timeEnd('Update setting diagnosis'); - console.log('Ending diagnosis'); } export async function clearPythonPathInWorkspaceFolder(resource: string | Uri) { @@ -169,28 +141,6 @@ function getWorkspaceRoot() { 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): PythonInterpreter | undefined { - return; - } - public async setWorkspaceInterpreter( - _resource: Uri, - _interpreter: PythonInterpreter | 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) => { @@ -232,7 +182,7 @@ async function setAutoSaveDelay(resource: string | Uri | undefined, config: Conf async function setPythonPathInWorkspace( resource: string | Uri | undefined, config: ConfigurationTarget, - pythonPath?: string + pythonPath?: string, ) { const vscode = require('vscode') as typeof import('vscode'); if (config === vscode.ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { @@ -240,11 +190,11 @@ async function setPythonPathInWorkspace( } 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 value = settings.inspect<string>('defaultInterpreterPath'); const prop: 'workspaceFolderValue' | 'workspaceValue' = config === vscode.ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; if (value && value[prop] !== pythonPath) { - await settings.update('pythonPath', pythonPath, config); + await settings.update('defaultInterpreterPath', pythonPath, config); await disposePythonSettings(); } } @@ -252,8 +202,8 @@ 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) + pythonConfig.update('defaultInterpreterPath', undefined, true), + pythonConfig.update('defaultInterpreterPath', undefined, true), ]); await disposePythonSettings(); } @@ -274,7 +224,7 @@ export async function deleteFile(file: string) { export async function deleteFiles(globPattern: string) { const items = await new Promise<string[]>((resolve, reject) => { - glob(globPattern, (ex, files) => (ex ? reject(ex) : resolve(files))); + glob.default(globPattern, (ex, files) => (ex ? reject(ex) : resolve(files))); }); return Promise.all(items.map((item) => fs.remove(item).catch(noop))); @@ -283,7 +233,7 @@ 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'; @@ -342,10 +292,9 @@ export function correctPathForOsType(pathToCorrect: string, os?: OSType): string * @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 proc = await import('../client/common/process/proc.js'); - const pythonProcRunner = procService ? procService : new proc.ProcessService(new decoder.BufferDecoder()); + const pythonProcRunner = procService ? procService : new proc.ProcessService(); const pyVerArgs = ['-c', 'import sys;print("{0}.{1}.{2}".format(*sys.version_info[:3]))']; return pythonProcRunner @@ -438,7 +387,7 @@ export async function isPythonVersionInProcess(procService?: IProcessService, .. return isVersionInList(currentPyVersion, ...versions); } else { console.error( - `Failed to determine the current Python version when comparing against list [${versions.join(', ')}].` + `Failed to determine the current Python version when comparing against list [${versions.join(', ')}].`, ); return false; } @@ -471,13 +420,13 @@ export async function isPythonVersion(...versions: string[]): Promise<boolean> { return isVersionInList(currentPyVersion, ...versions); } else { console.error( - `Failed to determine the current Python version when comparing against list [${versions.join(', ')}].` + `Failed to determine the current Python version when comparing against list [${versions.join(', ')}].`, ); return false; } } -export interface IExtensionTestApi extends IExtensionApi { +export interface IExtensionTestApi extends PythonExtension, ProposedExtensionAPI { serviceContainer: IServiceContainer; serviceManager: IServiceManager; } @@ -487,7 +436,7 @@ export async function unzip(zipFile: string, targetFolder: string): Promise<void return new Promise<void>((resolve, reject) => { const zip = new StreamZip({ file: zipFile, - storeEntries: true + storeEntries: true, }); zip.on('ready', async () => { zip.extract('extension', targetFolder, (err: any) => { @@ -503,22 +452,17 @@ export async function unzip(zipFile: string, targetFolder: string): Promise<void } /** * 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 + errorMessage: string, ): Promise<void> { return new Promise<void>(async (resolve, reject) => { const timeout = setTimeout(() => { clearTimeout(timeout); - // tslint:disable-next-line: no-use-before-declare + + // eslint-disable-next-line @typescript-eslint/no-use-before-define clearTimeout(timer); reject(new Error(errorMessage)); }, timeoutMs); @@ -541,12 +485,11 @@ export async function retryIfFail<T>(fn: () => Promise<T>, timeoutMs: number = 6 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; + lastEx = ex as Error | undefined; } await sleep(10); } @@ -560,87 +503,10 @@ 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'); + assert.ok(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. * @@ -648,8 +514,8 @@ export class FakeClock { * 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. + * assert.strictEqual(handler.first, 'Args Passed to first onDidSave') + * assert.strictEqual(handler.count, 1)// Only one should have been fired. */ export class TestEventHandler<T extends void | any = any> implements IDisposable { public get fired() { @@ -671,7 +537,7 @@ export class TestEventHandler<T extends void | any = any> implements IDisposable 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); @@ -689,14 +555,14 @@ export class TestEventHandler<T extends void | any = any> implements IDisposable await waitForCondition( async () => this.count === numberOfTimesFired, waitPeriod, - `${this.eventNameForErrorMessages} event fired ${this.count}, expected ${numberOfTimesFired}` + `${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}.` + `${this.eventNameForErrorMessages} event fired ${this.count}, expected at least ${numberOfTimesFired}.`, ); } public atIndex(index: number): T { @@ -715,8 +581,7 @@ export class TestEventHandler<T extends void | any = any> implements IDisposable export function createEventHandler<T, K extends keyof T>( obj: T, eventName: K, - dispoables: IDisposable[] = [] + 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/createNewFileCommand.unit.test.ts b/src/test/common/application/commands/createNewFileCommand.unit.test.ts new file mode 100644 index 000000000000..c50c7f729148 --- /dev/null +++ b/src/test/common/application/commands/createNewFileCommand.unit.test.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { TextDocument } from 'vscode'; +import { Commands } from '../../../../client/common/constants'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { CreatePythonFileCommandHandler } from '../../../../client/common/application/commands/createPythonFile'; + +suite('Create New Python File Commmand', () => { + let createNewFileCommandHandler: CreatePythonFileCommandHandler; + let cmdManager: ICommandManager; + let workspaceService: IWorkspaceService; + let appShell: IApplicationShell; + + setup(async () => { + cmdManager = mock(CommandManager); + workspaceService = mock(WorkspaceService); + appShell = mock(ApplicationShell); + + createNewFileCommandHandler = new CreatePythonFileCommandHandler( + instance(cmdManager), + instance(workspaceService), + instance(appShell), + [], + ); + when(workspaceService.openTextDocument(deepEqual({ language: 'python' }))).thenReturn( + Promise.resolve(({} as unknown) as TextDocument), + ); + await createNewFileCommandHandler.activate(); + }); + + test('Create Python file command is registered', async () => { + verify(cmdManager.registerCommand(Commands.CreateNewFile, anything(), anything())).once(); + }); + test('Create a Python file if command is executed', async () => { + await createNewFileCommandHandler.createPythonFile(); + verify(workspaceService.openTextDocument(deepEqual({ language: 'python' }))).once(); + verify(appShell.showTextDocument(anything())).once(); + }); +}); diff --git a/src/test/common/application/commands/issueTemplate.md b/src/test/common/application/commands/issueTemplate.md new file mode 100644 index 000000000000..a95af90ff7fe --- /dev/null +++ b/src/test/common/application/commands/issueTemplate.md @@ -0,0 +1,29 @@ +<!-- Please fill in all XXX markers --> +# Behaviour + +XXX + +## Steps to reproduce: + +1. XXX + +<!-- +**After** creating the issue on GitHub, you can add screenshots and GIFs of what is happening. Consider tools like https://www.cockos.com/licecap/, https://github.com/phw/peek or https://www.screentogif.com/ for GIF creation. +--> + +<!-- **NOTE**: Please do provide logs from Python Output panel. --> +# Diagnostic data + +<details> + +<summary>Output for <code>Python</code> in the <code>Output</code> panel (<code>View</code>→<code>Output</code>, change the drop-down the upper-right of the <code>Output</code> panel to <code>Python</code>) +</summary> + +<p> + +``` +XXX +``` + +</p> +</details> diff --git a/src/test/common/application/commands/issueUserDataTemplateVenv1.md b/src/test/common/application/commands/issueUserDataTemplateVenv1.md new file mode 100644 index 000000000000..2353d7b9f181 --- /dev/null +++ b/src/test/common/application/commands/issueUserDataTemplateVenv1.md @@ -0,0 +1,30 @@ +- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0 +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv +- Value of the `python.languageServer` setting: Pylance + +<details> +<summary>User Settings</summary> +<p> + +``` + +experiments +• enabled: false +• optInto: [] +• optOutFrom: [] + +venvPath: "<placeholder>" + +pipenvPath: "<placeholder>" + +``` +</p> +</details> + +<details> +<summary>Installed Extensions</summary> + +|Extension Name|Extension Id|Version| +|---|---|---| +|python|ms-|2020.2| +</details> diff --git a/src/test/common/application/commands/issueUserDataTemplateVenv2.md b/src/test/common/application/commands/issueUserDataTemplateVenv2.md new file mode 100644 index 000000000000..98ff2a880cdf --- /dev/null +++ b/src/test/common/application/commands/issueUserDataTemplateVenv2.md @@ -0,0 +1,27 @@ +- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0 +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv +- Value of the `python.languageServer` setting: Pylance + +<details> +<summary>User Settings</summary> +<p> + +``` +Multiroot scenario, following user settings may not apply: + +experiments +• enabled: false + +venvPath: "<placeholder>" + +``` +</p> +</details> + +<details> +<summary>Installed Extensions</summary> + +|Extension Name|Extension Id|Version| +|---|---|---| +|python|ms-|2020.2| +</details> diff --git a/src/test/common/application/commands/reloadCommand.unit.test.ts b/src/test/common/application/commands/reloadCommand.unit.test.ts index 2606cf18da7d..dfcc6a4ad434 100644 --- a/src/test/common/application/commands/reloadCommand.unit.test.ts +++ b/src/test/common/application/commands/reloadCommand.unit.test.ts @@ -28,34 +28,34 @@ suite('Common Commands ReloadCommand', () => { }); 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(); + 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); + + when(appShell.showInformationMessage(message, Common.reload)).thenResolve(Common.reload as any); await commandHandler.call(reloadCommandHandler, message); - verify(appShell.showInformationMessage(message, Common.reload())).once(); + 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(); + when(appShell.showInformationMessage(message, Common.reload)).thenResolve(); await commandHandler.call(reloadCommandHandler, message); - verify(appShell.showInformationMessage(message, Common.reload())).once(); + verify(appShell.showInformationMessage(message, Common.reload)).once(); verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); }); }); diff --git a/src/test/common/application/commands/reportIssueCommand.unit.test.ts b/src/test/common/application/commands/reportIssueCommand.unit.test.ts new file mode 100644 index 000000000000..175a43d14007 --- /dev/null +++ b/src/test/common/application/commands/reportIssueCommand.unit.test.ts @@ -0,0 +1,188 @@ +/* eslint-disable global-require */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { expect } from 'chai'; +import { WorkspaceFolder } from 'vscode-languageserver-protocol'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import * as Telemetry from '../../../../client/telemetry'; +import { LanguageServerType } from '../../../../client/activation/types'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { ReportIssueCommandHandler } from '../../../../client/common/application/commands/reportIssueCommand'; +import { + IApplicationEnvironment, + ICommandManager, + IWorkspaceService, +} from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { MockWorkspaceConfiguration } from '../../../mocks/mockWorkspaceConfig'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { Commands, EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { AllCommands } from '../../../../client/common/application/commands'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IConfigurationService } from '../../../../client/common/types'; +import { EventName } from '../../../../client/telemetry/constants'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as extensionsApi from '../../../../client/common/vscodeApis/extensionsApi'; + +suite('Report Issue Command', () => { + let reportIssueCommandHandler: ReportIssueCommandHandler; + let cmdManager: ICommandManager; + let workspaceService: IWorkspaceService; + let interpreterService: IInterpreterService; + let configurationService: IConfigurationService; + let appEnvironment: IApplicationEnvironment; + let expectedIssueBody: string; + let getExtensionsStub: sinon.SinonStub; + + setup(async () => { + workspaceService = mock(WorkspaceService); + cmdManager = mock(CommandManager); + interpreterService = mock(InterpreterService); + configurationService = mock(ConfigurationService); + appEnvironment = mock<IApplicationEnvironment>(); + getExtensionsStub = sinon.stub(extensionsApi, 'getExtensions'); + + when(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).thenResolve(); + when(workspaceService.getConfiguration('python')).thenReturn( + new MockWorkspaceConfiguration({ + languageServer: LanguageServerType.Node, + }), + ); + const interpreter = ({ + envType: EnvironmentType.Venv, + version: { raw: '3.9.0' }, + } as unknown) as PythonEnvironment; + when(interpreterService.getActiveInterpreter()).thenResolve(interpreter); + when(configurationService.getSettings()).thenReturn({ + experiments: { + enabled: false, + optInto: [], + optOutFrom: [], + }, + initialize: true, + venvPath: 'path', + pipenvPath: 'pipenv', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + cmdManager = mock(CommandManager); + + reportIssueCommandHandler = new ReportIssueCommandHandler( + instance(cmdManager), + instance(workspaceService), + instance(interpreterService), + instance(configurationService), + instance(appEnvironment), + ); + await reportIssueCommandHandler.activate(); + + const issueTemplatePath = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'common', + 'application', + 'commands', + 'issueTemplate.md', + ); + expectedIssueBody = fs.readFileSync(issueTemplatePath, 'utf8'); + + getExtensionsStub.returns([ + { + id: 'ms-python.python', + packageJSON: { + displayName: 'Python', + version: '2020.2', + name: 'python', + publisher: 'ms-python', + }, + }, + ]); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test if issue body is filled correctly when including all the settings', async () => { + await reportIssueCommandHandler.openReportIssue(); + + const userDataTemplatePath = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'common', + 'application', + 'commands', + 'issueUserDataTemplateVenv1.md', + ); + const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); + + const args: [string, { extensionId: string; issueBody: string; extensionData: string }] = capture< + AllCommands, + { extensionId: string; issueBody: string; extensionData: string } + >(cmdManager.executeCommand).last(); + + verify(cmdManager.registerCommand(Commands.ReportIssue, anything(), anything())).once(); + verify(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).once(); + expect(args[0]).to.be.equal('workbench.action.openIssueReporter'); + const { issueBody, extensionData } = args[1]; + expect(issueBody).to.be.equal(expectedIssueBody); + expect(extensionData).to.be.equal(expectedData); + }); + + test('Test if issue body is filled when only including settings which are explicitly set', async () => { + // eslint-disable-next-line import/no-dynamic-require + when(appEnvironment.packageJson).thenReturn(require(path.join(EXTENSION_ROOT_DIR, 'package.json'))); + when(workspaceService.workspaceFolders).thenReturn([ + instance(mock(WorkspaceFolder)), + instance(mock(WorkspaceFolder)), + ]); // Multiroot scenario + reportIssueCommandHandler = new ReportIssueCommandHandler( + instance(cmdManager), + instance(workspaceService), + instance(interpreterService), + instance(configurationService), + instance(appEnvironment), + ); + await reportIssueCommandHandler.activate(); + await reportIssueCommandHandler.openReportIssue(); + + const userDataTemplatePath = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'common', + 'application', + 'commands', + 'issueUserDataTemplateVenv2.md', + ); + const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); + + const args: [string, { extensionId: string; issueBody: string; extensionData: string }] = capture< + AllCommands, + { extensionId: string; issueBody: string; extensionData: string } + >(cmdManager.executeCommand).last(); + + verify(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).once(); + expect(args[0]).to.be.equal('workbench.action.openIssueReporter'); + const { issueBody, extensionData } = args[1]; + expect(issueBody).to.be.equal(expectedIssueBody); + expect(extensionData).to.be.equal(expectedData); + }); + test('Should send telemetry event when run Report Issue Command', async () => { + const sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent'); + await reportIssueCommandHandler.openReportIssue(); + + sinon.assert.calledWith(sendTelemetryStub, EventName.USE_REPORT_ISSUE_COMMAND); + sinon.restore(); + }); +}); diff --git a/src/test/common/application/progressService.unit.test.ts b/src/test/common/application/progressService.unit.test.ts new file mode 100644 index 000000000000..b9c49ccb4060 --- /dev/null +++ b/src/test/common/application/progressService.unit.test.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import { CancellationToken, Progress, ProgressLocation, ProgressOptions } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { ProgressService } from '../../../client/common/application/progressService'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { createDeferred, createDeferredFromPromise, Deferred, sleep } from '../../../client/common/utils/async'; + +type ProgressTask<R> = ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, +) => Thenable<R>; + +suite('Progress Service', () => { + let refreshDeferred: Deferred<void>; + let shell: ApplicationShell; + let progressService: ProgressService; + setup(() => { + refreshDeferred = createDeferred<void>(); + shell = mock<IApplicationShell>(); + progressService = new ProgressService(instance(shell)); + }); + teardown(() => { + refreshDeferred.resolve(); + }); + test('Display discovering message when refreshing interpreters for the first time', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const expectedOptions = { title: 'message', location: ProgressLocation.Window }; + + progressService.showProgress(expectedOptions); + + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + assert.deepEqual(options, expectedOptions); + }); + + test('Progress message is hidden when loading has completed', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const options = { title: 'message', location: ProgressLocation.Window }; + progressService.showProgress(options); + + const callback = capture(shell.withProgress as never).last()[1] as ProgressTask<void>; + const promise = callback(undefined as never, undefined as never); + const deferred = createDeferredFromPromise(promise as Promise<void>); + await sleep(1); + expect(deferred.completed).to.be.equal(false, 'Progress disappeared before hiding it'); + progressService.hideProgress(); + await sleep(1); + expect(deferred.completed).to.be.equal(true, 'Progress did not disappear'); + }); +}); diff --git a/src/test/common/asyncDump.ts b/src/test/common/asyncDump.ts deleted file mode 100644 index b92b0231ced9..000000000000 --- a/src/test/common/asyncDump.ts +++ /dev/null @@ -1,9 +0,0 @@ -'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 index 44844516ed2d..a8b4961f037c 100644 --- a/src/test/common/configSettings.test.ts +++ b/src/test/common/configSettings.test.ts @@ -1,11 +1,10 @@ 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 { getExtensionSettings } from '../extensionSettings'; import { initialize } from './../initialize'; +import { isWindows } from '../../client/common/utils/platform'; const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); @@ -15,7 +14,7 @@ suite('Configuration Settings', () => { 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) => { @@ -26,31 +25,13 @@ suite('Configuration Settings', () => { 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( + if (key.endsWith('Path') && isWindows()) { + assert.strictEqual( 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 }; - delete workspaceSettingsWithoutPath.tagFilePath; - const pythonSettingValueWithoutPath = { ...((pythonSettingValue as {}) as IWorkspaceSymbolSettings) }; - delete pythonSettingValueWithoutPath.tagFilePath; - assert.deepEqual( - workspaceSettingsWithoutPath, - pythonSettingValueWithoutPath, - `Setting ${key} not the same` + `Setting ${key} not the same`, ); } }); diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index e83a69742aee..8a2a90b288a3 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -13,36 +11,40 @@ 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 { IExperimentService, IInterpreterPathService } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; -import { IInterpreterSecurityService } from '../../../client/interpreter/autoSelection/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; -const untildify = require('untildify'); +import { untildify } from '../../../client/common/helpers'; +import { MockExtensions } from '../../mocks/extensions'; suite('Python Settings - pythonPath', () => { class CustomPythonSettings extends PythonSettings { public update(settings: WorkspaceConfiguration) { return super.update(settings); } + + // eslint-disable-next-line class-methods-use-this protected getPythonExecutable(pythonPath: string) { return pythonPath; } - protected initialize() { + + // eslint-disable-next-line class-methods-use-this + public initialize() { noop(); } } let configSettings: CustomPythonSettings; let workspaceService: typemoq.IMock<IWorkspaceService>; - let experimentsManager: typemoq.IMock<IExperimentsManager>; + let experimentsManager: typemoq.IMock<IExperimentService>; 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>(); + experimentsManager = typemoq.Mock.ofType<IExperimentService>(); 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' })); @@ -54,50 +56,66 @@ suite('Python Settings - pythonPath', () => { sinon.restore(); }); - test('Python Path from settings.json is used', () => { - configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + test('Python Path from settings is used', () => { const pythonPath = 'This is the python Path'; - pythonSettings - .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) - .returns(() => pythonPath) - .verifiable(typemoq.Times.atLeast(1)); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => pythonPath); + configSettings = new CustomPythonSettings( + undefined, + new MockAutoSelectionService(), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); 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()); + test("Python Path from settings is used and relative path starting with '~' will be resolved from home directory", () => { 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)); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => pythonPath); + configSettings = new CustomPythonSettings( + undefined, + new MockAutoSelectionService(), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); 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()); + test("Python Path from settings is used and relative path starting with '.' will be resolved from workspace folder", () => { 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)); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => pythonPath); + const workspaceFolderUri = Uri.file(__dirname); + configSettings = new CustomPythonSettings( + workspaceFolderUri, + new MockAutoSelectionService(), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); 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()); + test('Python Path from settings is used and ${workspacecFolder} value will be resolved from workspace folder', () => { 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)); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => pythonPath); + const workspaceFolderUri = Uri.file(__dirname); + configSettings = new CustomPythonSettings( + workspaceFolderUri, + new MockAutoSelectionService(), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); configSettings.update(pythonSettings.object); expect(configSettings.pythonPath).to.be.equal(path.join(workspaceFolderUri.fsPath, 'This is the python Path')); @@ -105,152 +123,99 @@ suite('Python Settings - pythonPath', () => { 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)); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => pythonPath); + configSettings = new CustomPythonSettings( + workspaceFolderUri, + instance(selectionService), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); 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", () => { + test("If a workspace is opened and if we don't have a custom python path but 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 interpreter = { path: pythonPath } as PythonEnvironment; 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(); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => 'python'); configSettings = new CustomPythonSettings( - resource, + workspaceFolderUri, instance(selectionService), workspaceService.object, - experimentsManager.object, interpreterPathService.object, - interpreterSecurityService.object + undefined, + new MockExtensions(), ); - 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(); + verify(selectionService.setWorkspaceInterpreter(workspaceFolderUri, interpreter)).once(); // Verify we set the autoselected interpreter }); - 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'); + test("If no workspace is opened and we don't have a custom python path but 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 interpreter = { path: pythonPath } as PythonEnvironment; + const workspaceFolderUri = Uri.file(__dirname); 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(); + when(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).thenReturn(interpreter); + when(selectionService.setWorkspaceInterpreter(workspaceFolderUri, anything())).thenResolve(); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => 'python'); + configSettings = new CustomPythonSettings( - resource, + workspaceFolderUri, instance(selectionService), workspaceService.object, - experimentsManager.object, interpreterPathService.object, - interpreterSecurityService.object + undefined, + new MockExtensions(), ); - 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(); + expect(configSettings.pythonPath).to.be.equal(pythonPath); }); - test('If user is in Deprecate Python Path experiment, use the new API to fetch Python Path', () => { - const resource = Uri.parse('a'); + test("If we don't have a custom default 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 = { path: pythonPath } as PythonEnvironment; + const workspaceFolderUri = Uri.file(__dirname); + const selectionService = mock(MockAutoSelectionService); + when(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).thenReturn(interpreter); + configSettings = new CustomPythonSettings( - resource, - new MockAutoSelectionService(), + workspaceFolderUri, + instance(selectionService), workspaceService.object, - experimentsManager.object, - interpreterPathService.object + interpreterPathService.object, + undefined, + new MockExtensions(), ); - 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()); + interpreterPathService.setup((i) => i.get(typemoq.It.isAny())).returns(() => 'custom'); + pythonSettings.setup((p) => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python'); configSettings.update(pythonSettings.object); - expect(configSettings.pythonPath).to.be.equal(pythonPath); - experimentsManager.verifyAll(); - interpreterPathService.verifyAll(); - pythonSettings.verifyAll(); + expect(configSettings.defaultInterpreterPath).to.be.equal(pythonPath); }); - test('If user is not in Deprecate Python Path experiment, use the settings to fetch Python Path', () => { + test("If 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 = { path: pythonPath } as PythonEnvironment; + const selectionService = mock(MockAutoSelectionService); + when(selectionService.getAutoSelectedInterpreter(resource)).thenReturn(interpreter); + when(selectionService.setWorkspaceInterpreter(resource, anything())).thenResolve(); configSettings = new CustomPythonSettings( resource, - new MockAutoSelectionService(), + instance(selectionService), workspaceService.object, - experimentsManager.object, - interpreterPathService.object + interpreterPathService.object, + undefined, + new MockExtensions(), ); - 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()); + interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); configSettings.update(pythonSettings.object); expect(configSettings.pythonPath).to.be.equal(pythonPath); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 6a6cbc07cf91..65afc782d7bb 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -3,53 +3,74 @@ 'use strict'; -// tslint:disable:no-any - -import { assert, expect } from 'chai'; +import { 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 { IApplicationEnvironment } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; import { PythonSettings } from '../../../client/common/configSettings'; +import { InterpreterPathService } from '../../../client/common/interpreterPathService'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; import { - IAnalysisSettings, IAutoCompleteSettings, - IDataScienceSettings, IExperiments, - IFormattingSettings, - ILintingSettings, - ILoggingSettings, - ISortImportSettings, + IInterpreterSettings, ITerminalSettings, - ITestingSettings, - IWorkspaceSymbolSettings } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; +import { ITestingSettings } from '../../../client/testing/configuration/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; +import { MockMemento } from '../../mocks/mementos'; +import { untildify } from '../../../client/common/helpers'; +import { MockExtensions } from '../../mocks/extensions'; -// 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() { + public initialize() { noop(); } } let config: TypeMoq.IMock<WorkspaceConfiguration>; let expected: CustomPythonSettings; let settings: CustomPythonSettings; + let extensions: MockExtensions; setup(() => { sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); - config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); - expected = new CustomPythonSettings(undefined, new MockAutoSelectionService()); - settings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Loose); + + const workspaceService = new WorkspaceService(); + const workspaceMemento = new MockMemento(); + const globalMemento = new MockMemento(); + extensions = new MockExtensions(); + const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); + expected = new CustomPythonSettings( + undefined, + new MockAutoSelectionService(), + workspaceService, + new InterpreterPathService(persistentStateFactory, workspaceService, [], { + remoteName: undefined, + } as IApplicationEnvironment), + { defaultLSType: LanguageServerType.Jedi }, + extensions, + ); + settings = new CustomPythonSettings( + undefined, + new MockAutoSelectionService(), + workspaceService, + new InterpreterPathService(persistentStateFactory, workspaceService, [], { + remoteName: undefined, + } as IApplicationEnvironment), + { defaultLSType: LanguageServerType.Jedi }, + extensions, + ); expected.defaultInterpreterPath = 'python'; }); @@ -62,62 +83,46 @@ suite('Python Settings', async () => { for (const name of [ 'pythonPath', 'venvPath', + 'activeStateToolPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', - 'insidersChannel', + 'pixiToolPath', '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']) { + for (const name of ['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<IInterpreterSettings>('interpreter')).returns(() => sourceSettings.interpreter); 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); } @@ -136,31 +141,38 @@ suite('Python Settings', async () => { suite('String settings', async () => { [ - 'pythonPath', 'venvPath', + 'activeStateToolPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', - 'insidersChannel', - 'defaultInterpreterPath' + 'pixiToolPath', + 'defaultInterpreterPath', ].forEach(async (settingName) => { testIfValueIsUpdated(settingName, 'stringValue'); }); }); suite('Boolean settings', async () => { - ['downloadLanguageServer', 'autoUpdateLanguageServer', 'globalModuleInstallation'].forEach( - async (settingName) => { - testIfValueIsUpdated(settingName, true); - } - ); + ['globalModuleInstallation'].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, true); + }); }); - suite('Number settings', async () => { - ['jediMemoryLimit'].forEach(async (settingName) => { - testIfValueIsUpdated(settingName, 1001); + test('Interpreter settings object', () => { + initializeConfig(expected); + config + .setup((c) => c.get<string>('condaPath')) + .returns(() => expected.condaPath) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + expect(settings.interpreter).to.deep.equal({ + infoVisibility: 'onPythonRelated', }); + config.verifyAll(); }); test('condaPath updated', () => { @@ -193,7 +205,11 @@ suite('Python Settings', async () => { config.verifyAll(); }); - function testLanguageServer(languageServer: LanguageServerType, expectedValue: LanguageServerType) { + function testLanguageServer( + languageServer: LanguageServerType, + expectedValue: LanguageServerType, + isDefault: boolean, + ) { test(languageServer, () => { expected.pythonPath = 'python3'; expected.languageServer = languageServer; @@ -206,25 +222,75 @@ suite('Python Settings', async () => { settings.update(config.object); expect(settings.languageServer).to.be.equal(expectedValue); + expect(settings.languageServerIsDefault).to.be.equal(isDefault); config.verifyAll(); }); } suite('languageServer settings', async () => { - Object.values(LanguageServerType).forEach(async (languageServer) => { - testLanguageServer(languageServer, languageServer); + const values = [ + { ls: LanguageServerType.Jedi, expected: LanguageServerType.Jedi, default: false }, + { ls: LanguageServerType.JediLSP, expected: LanguageServerType.Jedi, default: false }, + { ls: LanguageServerType.Microsoft, expected: LanguageServerType.Jedi, default: true }, + { ls: LanguageServerType.Node, expected: LanguageServerType.Node, default: false }, + { ls: LanguageServerType.None, expected: LanguageServerType.None, default: false }, + ]; + + values.forEach((v) => { + testLanguageServer(v.ls, v.expected, v.default); }); - testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi); + testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi, true); + }); + + function testPyreflySettings(pyreflyInstalled: boolean, pyreflyDisabled: boolean, languageServerDisabled: boolean) { + test(`pyrefly ${pyreflyInstalled ? 'installed' : 'not installed'} and ${ + pyreflyDisabled ? 'disabled' : 'enabled' + }`, () => { + if (pyreflyInstalled) { + extensions.extensionIdsToFind = ['meta.pyrefly']; + } else { + extensions.extensionIdsToFind = []; + } + config.setup((c) => c.get<boolean>('pyrefly.disableLanguageServices')).returns(() => pyreflyDisabled); + + config + .setup((c) => c.get<string>('languageServer')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + if (languageServerDisabled) { + expect(settings.languageServer).to.equal(LanguageServerType.None); + } else { + expect(settings.languageServer).not.to.equal(LanguageServerType.None); + } + expect(settings.languageServerIsDefault).to.equal(true); + config.verifyAll(); + }); + } + + suite('pyrefly languageServer settings', async () => { + const values = [ + { pyreflyInstalled: true, pyreflyDisabled: false, languageServerDisabled: true }, + { pyreflyInstalled: true, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: false, languageServerDisabled: false }, + ]; + + values.forEach((v) => { + testPyreflySettings(v.pyreflyInstalled, v.pyreflyDisabled, v.languageServerDisabled); + }); }); function testExperiments(enabled: boolean) { expected.pythonPath = 'python3'; - // tslint:disable-next-line:no-any + expected.experiments = { enabled, optInto: [], - optOutFrom: [] + optOutFrom: [], }; initializeConfig(expected); config @@ -235,7 +301,6 @@ suite('Python Settings', async () => { 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(); @@ -243,113 +308,4 @@ suite('Python Settings', async () => { 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 index 5bbd24a9fa8d..c57617b2a610 100644 --- a/src/test/common/configuration/service.test.ts +++ b/src/test/common/configuration/service.test.ts @@ -2,12 +2,11 @@ // Licensed under the MIT License. import { expect } from 'chai'; import { workspace } from 'vscode'; -import { IAsyncDisposableRegistry, IConfigurationService } from '../../../client/common/types'; +import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; -import { getExtensionSettings } from '../../common'; +import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from '../../initialize'; -// tslint:disable-next-line:max-func-body-length suite('Configuration Service', () => { let serviceContainer: IServiceContainer; suiteSetup(async () => { @@ -22,16 +21,18 @@ suite('Configuration Service', () => { }); test('Ensure async registry works', async () => { - const asyncRegistry = serviceContainer.get<IAsyncDisposableRegistry>(IAsyncDisposableRegistry); - let disposed = false; + const asyncRegistry = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); + let subs = serviceContainer.get<IExtensionContext>(IExtensionContext).subscriptions; + const oldLength = subs.length; 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"); + subs = serviceContainer.get<IExtensionContext>(IExtensionContext).subscriptions; + const newLength = subs.length; + expect(newLength).to.be.equal(oldLength + 1, 'Subscription not added'); + // serviceContainer subscriptions are not disposed of as this breaks other tests that use the service container. }); }); diff --git a/src/test/common/configuration/service.unit.test.ts b/src/test/common/configuration/service.unit.test.ts index 887a0779acf7..19f57173f10a 100644 --- a/src/test/common/configuration/service.unit.test.ts +++ b/src/test/common/configuration/service.unit.test.ts @@ -9,41 +9,29 @@ 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 { IInterpreterPathService } from '../../../client/common/types'; +import { IInterpreterAutoSelectionService } 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' + 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); }); @@ -56,13 +44,9 @@ suite('Configuration Service', () => { } test('Fetching settings goes as expected', () => { - const interpreterAutoSelectionProxyService = TypeMoq.Mock.ofType<IInterpreterAutoSeletionProxyService>(); + const interpreterAutoSelectionProxyService = TypeMoq.Mock.ofType<IInterpreterAutoSelectionService>(); serviceContainer - .setup((s) => s.get(IInterpreterSecurityService)) - .returns(() => interpreterSecurityService.object) - .verifiable(TypeMoq.Times.once()); - serviceContainer - .setup((s) => s.get(IInterpreterAutoSeletionProxyService)) + .setup((s) => s.get(IInterpreterAutoSelectionService)) .returns(() => interpreterAutoSelectionProxyService.object) .verifiable(TypeMoq.Times.once()); const settings = configService.getSettings(); @@ -70,10 +54,11 @@ suite('Configuration Service', () => { }); 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.inspect('setting')) + .returns(() => ({ globalValue: 'globalValue', key: 'setting' })); workspaceConfig .setup((w) => w.update('setting', 'globalValue', ConfigurationTarget.Global)) .returns(() => Promise.resolve()) @@ -85,10 +70,11 @@ suite('Configuration Service', () => { }); 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.inspect('setting')) + .returns(() => ({ globalValue: 'globalValue', key: 'setting' })); workspaceConfig .setup((w) => w.update('setting', 'newGlobalValue', ConfigurationTarget.Global)) .returns(() => Promise.resolve()) @@ -100,10 +86,11 @@ suite('Configuration Service', () => { }); 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.inspect('setting')) + .returns(() => ({ workspaceValue: 'workspaceValue', key: 'setting' })); workspaceConfig .setup((w) => w.update('setting', 'workspaceValue', ConfigurationTarget.Workspace)) .returns(() => Promise.resolve()) @@ -115,10 +102,11 @@ suite('Configuration Service', () => { }); 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.inspect('setting')) + .returns(() => ({ workspaceValue: 'workspaceValue', key: 'setting' })); workspaceConfig .setup((w) => w.update('setting', 'newWorkspaceValue', ConfigurationTarget.Workspace)) .returns(() => Promise.resolve()) @@ -130,12 +118,11 @@ suite('Configuration Service', () => { }); 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)); + + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue', key: 'setting' })); workspaceConfig .setup((w) => w.update('setting', 'workspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) .returns(() => Promise.resolve()) @@ -145,19 +132,18 @@ suite('Configuration Service', () => { 'setting', 'workspaceFolderValue', resource, - ConfigurationTarget.WorkspaceFolder + 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)); + + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue', key: 'setting' })); workspaceConfig .setup((w) => w.update('setting', 'newWorkspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) .returns(() => Promise.resolve()) @@ -167,30 +153,9 @@ suite('Configuration Service', () => { 'setting', 'newWorkspaceFolderValue', resource, - ConfigurationTarget.WorkspaceFolder + 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 deleted file mode 100644 index 878e2b1ff0d4..000000000000 --- a/src/test/common/crypto.unit.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -// 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 deleted file mode 100644 index eb6289ec41c5..000000000000 --- a/src/test/common/dotnet/compatibilityService.unit.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index d03d6113d7cc..000000000000 --- a/src/test/common/dotnet/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index 4e945d865edf..000000000000 --- a/src/test/common/dotnet/services/linuxCompatibilityService.unit.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index ed3f9557101f..000000000000 --- a/src/test/common/dotnet/services/macCompatibilityService.unit.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index 7ab997b440b5..000000000000 --- a/src/test/common/dotnet/services/unknownOsCompatibilityService.unit.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -// 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 deleted file mode 100644 index bf7455b16708..000000000000 --- a/src/test/common/dotnet/services/winCompatibilityService.unit.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -// 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 index 602d6eefd1ed..cb04d3a90b38 100644 --- a/src/test/common/exitCIAfterTestReporter.ts +++ b/src/test/common/exitCIAfterTestReporter.ts @@ -7,8 +7,8 @@ // 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 fs from '../../client/common/platform/fs-paths'; + import * as net from 'net'; import * as path from 'path'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; @@ -25,7 +25,7 @@ async function connectToServer() { } const port = parseInt(await fs.readFile(portFile, 'utf-8'), 10); console.log(`Need to connect to port ${port}`); - return new Promise((resolve) => { + return new Promise<void>((resolve) => { try { client = new net.Socket(); client.connect({ port }, () => { diff --git a/src/test/common/experiments/manager.unit.test.ts b/src/test/common/experiments/manager.unit.test.ts deleted file mode 100644 index aa5801464f29..000000000000 --- a/src/test/common/experiments/manager.unit.test.ts +++ /dev/null @@ -1,1083 +0,0 @@ -// 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 index bdd07b278f8f..661efeaa8bb9 100644 --- a/src/test/common/experiments/service.unit.test.ts +++ b/src/test/common/experiments/service.unit.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-new */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -6,13 +7,20 @@ 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 { Disposable } from 'vscode-jsonrpc'; +// sinon can not create a stub if we just point to the exported module +import * as tasClient from 'vscode-tas-client/vscode-tas-client/VSCodeTasClient'; +import * as expService from 'vscode-tas-client'; +import { TargetPopulation } 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 { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { Channel } from '../../../client/common/constants'; import { ExperimentService } from '../../../client/common/experiments/service'; -import { IConfigurationService } from '../../../client/common/types'; -import { Experiments } from '../../../client/common/utils/localize'; +import { PersistentState } from '../../../client/common/persistentState'; +import { IPersistentStateFactory } from '../../../client/common/types'; +import { registerLogger } from '../../../client/logging'; +import { OutputChannelLogger } from '../../../client/logging/outputChannelLogger'; import * as Telemetry from '../../../client/telemetry'; import { EventName } from '../../../client/telemetry/constants'; import { PVSC_EXTENSION_ID_FOR_TESTS } from '../../constants'; @@ -21,87 +29,93 @@ import { MockMemento } from '../../mocks/mementos'; suite('Experimentation service', () => { const extensionVersion = '1.2.3'; + const dummyExperimentKey = 'experimentsKey'; - let configurationService: IConfigurationService; + let workspaceService: IWorkspaceService; let appEnvironment: IApplicationEnvironment; + let stateFactory: IPersistentStateFactory; let globalMemento: MockMemento; let outputChannel: MockOutputChannel; + let disposeLogger: Disposable; setup(() => { - configurationService = mock(ConfigurationService); appEnvironment = mock(ApplicationEnvironment); + workspaceService = mock(WorkspaceService); + stateFactory = mock<IPersistentStateFactory>(); globalMemento = new MockMemento(); + when(stateFactory.createGlobalPersistentState(anything(), anything())).thenReturn( + new PersistentState(globalMemento, dummyExperimentKey, { features: [] }), + ); outputChannel = new MockOutputChannel(''); + disposeLogger = registerLogger(new OutputChannelLogger(outputChannel)); }); teardown(() => { sinon.restore(); Telemetry._resetSharedProperties(); + disposeLogger.dispose(); }); function configureSettings(enabled: boolean, optInto: string[], optOutFrom: string[]) { - when(configurationService.getSettings(undefined)).thenReturn({ - experiments: { - enabled, - optInto, - optOutFrom - } - // tslint:disable-next-line: no-any + when(workspaceService.getConfiguration('python')).thenReturn({ + get: (key: string) => { + if (key === 'experiments.enabled') { + return enabled; + } + if (key === 'experiments.optInto') { + return optInto; + } + if (key === 'experiments.optOutFrom') { + return optOutFrom; + } + return undefined; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); } - function configureApplicationEnvironment(channel: Channel, version: string) { - when(appEnvironment.extensionChannel).thenReturn(channel); + function configureApplicationEnvironment(channel: Channel, version: string, contributes?: Record<string, unknown>) { + when(appEnvironment.channel).thenReturn(channel); when(appEnvironment.extensionName).thenReturn(PVSC_EXTENSION_ID_FOR_TESTS); - when(appEnvironment.packageJson).thenReturn({ version }); + when(appEnvironment.packageJson).thenReturn({ version, contributes }); } suite('Initialization', () => { - test('Users with a release version of the extension should be in the Public target population', () => { + test('Users with VS Code stable version 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 - ); + // eslint-disable-next-line no-new + new ExperimentService(instance(workspaceService), instance(appEnvironment), instance(stateFactory)); + // @ts-ignore I dont know how else to ignore this issue. sinon.assert.calledWithExactly( getExperimentationServiceStub, PVSC_EXTENSION_ID_FOR_TESTS, extensionVersion, - tasClient.TargetPopulation.Public, + sinon.match(TargetPopulation.Public), sinon.match.any, - globalMemento + globalMemento, ); }); - test('Users with an Insiders version of the extension should be the Insiders target population', () => { + test('Users with VS Code Insiders version 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 - ); + // eslint-disable-next-line no-new + new ExperimentService(instance(workspaceService), instance(appEnvironment), instance(stateFactory)); sinon.assert.calledWithExactly( getExperimentationServiceStub, PVSC_EXTENSION_ID_FOR_TESTS, extensionVersion, - tasClient.TargetPopulation.Insiders, + sinon.match(TargetPopulation.Insiders), sinon.match.any, - globalMemento + globalMemento, ); }); @@ -112,10 +126,9 @@ suite('Experimentation service', () => { configureApplicationEnvironment('stable', extensionVersion); const experimentService = new ExperimentService( - instance(configurationService), + instance(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); assert.deepEqual(experimentService._optInto, ['Foo - experiment']); @@ -127,173 +140,484 @@ suite('Experimentation service', () => { configureApplicationEnvironment('stable', extensionVersion); const experimentService = new ExperimentService( - instance(configurationService), + instance(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); assert.deepEqual(experimentService._optOutFrom, ['Foo - experiment']); }); - test('Experiment data in Memento storage should be logged if it starts with "python"', () => { + test('Experiment data in Memento storage should be logged if it starts with "python"', async () => { const experiments = ['ExperimentOne', 'pythonExperiment']; - globalMemento = mock(MockMemento); + globalMemento.update(dummyExperimentKey, { features: experiments }); configureSettings(true, [], []); - configureApplicationEnvironment('stable', extensionVersion); - // tslint:disable-next-line: no-any - when(globalMemento.get(anything(), anything())).thenReturn({ features: experiments } as any); + configureApplicationEnvironment('stable', extensionVersion, { configuration: { properties: {} } }); - // tslint:disable-next-line: no-unused-expression - new ExperimentService( - instance(configurationService), + const exp = new ExperimentService( + instance(workspaceService), instance(appEnvironment), - instance(globalMemento), - outputChannel + instance(stateFactory), ); - const output = `${Experiments.inGroup().format('pythonExperiment')}\n`; + await exp.activate(); + const output = "Experiment 'pythonExperiment' is active\n"; - assert.equal(outputChannel.output, output); + assert.strictEqual(outputChannel.output, output); }); }); - suite('In-experiment check', () => { + suite('In-experiment-sync check', () => { const experiment = 'Test Experiment - experiment'; - let telemetryEvents: { eventName: string; properties: object }[] = []; - let isCachedFlightEnabledStub: sinon.SinonStub; + let telemetryEvents: { eventName: string; properties: unknown }[] = []; + let getTreatmentVariable: sinon.SinonStub; let sendTelemetryEventStub: sinon.SinonStub; setup(() => { sendTelemetryEventStub = sinon .stub(Telemetry, 'sendTelemetryEvent') - .callsFake((eventName: string, _, properties: object) => { + .callsFake((eventName: string, _, properties: unknown) => { 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); + getTreatmentVariable = sinon.stub().returns(true); + sinon.stub(tasClient, 'getExperimentationService').returns(({ + getTreatmentVariable, + } as unknown) as expService.IExperimentationService); configureApplicationEnvironment('stable', extensionVersion); }); teardown(() => { telemetryEvents = []; + sinon.restore(); }); 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(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); - const result = await experimentService.inExperiment(experiment); + const result = experimentService.inExperimentSync(experiment); assert.isTrue(result); sinon.assert.notCalled(sendTelemetryEventStub); - sinon.assert.calledOnce(isCachedFlightEnabledStub); + sinon.assert.calledOnce(getTreatmentVariable); + }); + + test('If in control group, return false', async () => { + sinon.restore(); + sendTelemetryEventStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: string, _, properties: unknown) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }); + + // Control group returns false. + getTreatmentVariable = sinon.stub().returns(false); + sinon.stub(tasClient, 'getExperimentationService').returns(({ + getTreatmentVariable, + } as unknown) as expService.IExperimentationService); + + configureApplicationEnvironment('stable', extensionVersion); + + configureSettings(true, [], []); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isFalse(result); + sinon.assert.notCalled(sendTelemetryEventStub); + sinon.assert.calledOnce(getTreatmentVariable); }); test('If the experiment setting is disabled, inExperiment should return false', async () => { configureSettings(false, [], []); const experimentService = new ExperimentService( - instance(configurationService), + instance(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); - const result = await experimentService.inExperiment(experiment); + const result = experimentService.inExperimentSync(experiment); assert.isFalse(result); sinon.assert.notCalled(sendTelemetryEventStub); - sinon.assert.notCalled(isCachedFlightEnabledStub); + sinon.assert.notCalled(getTreatmentVariable); }); test('If the opt-in setting contains "All", inExperiment should return true', async () => { configureSettings(true, ['All'], []); const experimentService = new ExperimentService( - instance(configurationService), + instance(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); - const result = await experimentService.inExperiment(experiment); + const result = experimentService.inExperimentSync(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); + assert.strictEqual(telemetryEvents.length, 0); + }); + + test('If the opt-in setting contains `All`, inExperiment should check the value cached by the experiment service', async () => { + configureSettings(true, ['All'], []); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isTrue(result); + sinon.assert.notCalled(sendTelemetryEventStub); + sinon.assert.calledOnce(getTreatmentVariable); + }); + + test('If the opt-in setting contains `All` and the experiment setting is disabled, inExperiment should return false', async () => { + configureSettings(false, ['All'], []); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isFalse(result); + sinon.assert.notCalled(sendTelemetryEventStub); + sinon.assert.notCalled(getTreatmentVariable); }); 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(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); - const result = await experimentService.inExperiment(experiment); + const result = experimentService.inExperimentSync(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); + assert.strictEqual(telemetryEvents.length, 0); + sinon.assert.calledOnce(getTreatmentVariable); }); test('If the opt-out setting contains "All", inExperiment should return false', async () => { configureSettings(true, [], ['All']); const experimentService = new ExperimentService( - instance(configurationService), + instance(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); - const result = await experimentService.inExperiment(experiment); + const result = experimentService.inExperimentSync(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); + sinon.assert.notCalled(sendTelemetryEventStub); + sinon.assert.notCalled(getTreatmentVariable); + }); + + test('If the opt-out setting contains "All" and the experiment setting is enabled, inExperiment should return false', async () => { + configureSettings(true, [], ['All']); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isFalse(result); + sinon.assert.notCalled(sendTelemetryEventStub); + sinon.assert.notCalled(getTreatmentVariable); }); 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(workspaceService), instance(appEnvironment), - globalMemento, - outputChannel + instance(stateFactory), ); - const result = await experimentService.inExperiment(experiment); + const result = experimentService.inExperimentSync(experiment); assert.isFalse(result); - assert.equal(telemetryEvents.length, 1); - assert.deepEqual(telemetryEvents[0], { - eventName: EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, - properties: { expNameOptedOutOf: experiment } + assert.strictEqual(telemetryEvents.length, 0); + sinon.assert.notCalled(getTreatmentVariable); + }); + }); + + suite('Experiment value retrieval', () => { + const experiment = 'Test Experiment - experiment'; + let getTreatmentVariableStub: sinon.SinonStub; + + setup(() => { + getTreatmentVariableStub = sinon.stub().returns(Promise.resolve('value')); + sinon.stub(tasClient, 'getExperimentationService').returns(({ + getTreatmentVariable: getTreatmentVariableStub, + } as unknown) as expService.IExperimentationService); + + 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(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.strictEqual(result, 'value'); + sinon.assert.calledOnce(getTreatmentVariableStub); + }); + + test('If the experiment setting is disabled, getExperimentValue should return undefined', async () => { + configureSettings(false, [], []); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.isUndefined(result); + sinon.assert.notCalled(getTreatmentVariableStub); + }); + + test('If the opt-out setting contains "All", getExperimentValue should return undefined', async () => { + configureSettings(true, [], ['All']); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.isUndefined(result); + sinon.assert.notCalled(getTreatmentVariableStub); + }); + + test('If the opt-out setting contains the experiment name, getExperimentValue should return undefined', async () => { + configureSettings(true, [], [experiment]); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.isUndefined(result); + sinon.assert.notCalled(getTreatmentVariableStub); + }); + }); + + suite('Opt-in/out telemetry', () => { + let telemetryEvents: { eventName: string; properties: unknown }[] = []; + let sendTelemetryEventStub: sinon.SinonStub; + + setup(() => { + sendTelemetryEventStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: string, _, properties: unknown) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }); + + configureApplicationEnvironment('stable', extensionVersion); + }); + + teardown(() => { + telemetryEvents = []; + }); + + test('Telemetry should be sent when activating the ExperimentService instance', async () => { + configureSettings(true, [], []); + configureApplicationEnvironment('stable', extensionVersion, { configuration: { properties: {} } }); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + await experimentService.activate(); + + assert.strictEqual(telemetryEvents.length, 2); + assert.strictEqual(telemetryEvents[1].eventName, EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS); + sinon.assert.calledTwice(sendTelemetryEventStub); + }); + + test('The telemetry event properties should only be populated with valid experiment values', async () => { + const contributes = { + configuration: { + properties: { + 'python.experiments.optInto': { + items: { + enum: ['foo', 'bar'], + }, + }, + 'python.experiments.optOutFrom': { + items: { + enum: ['foo', 'bar'], + }, + }, + }, + }, + }; + configureSettings(true, ['foo', 'baz'], ['bar', 'invalid']); + configureApplicationEnvironment('stable', extensionVersion, contributes); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + await experimentService.activate(); + + const { properties } = telemetryEvents[1]; + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['foo']), + optedOutFrom: JSON.stringify(['bar']), + }); + }); + + test('Set telemetry properties to empty arrays if no experiments have been opted into or out from', async () => { + const contributes = { + configuration: { + properties: { + 'python.experiments.optInto': { + items: { + enum: ['foo', 'bar'], + }, + }, + 'python.experiments.optOutFrom': { + items: { + enum: ['foo', 'bar'], + }, + }, + }, + }, + }; + configureSettings(true, [], []); + configureApplicationEnvironment('stable', extensionVersion, contributes); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + await experimentService.activate(); + + const { properties } = telemetryEvents[1]; + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); + }); + + test('If the entered value for a setting contains "All", do not expand it to be a list of all experiments, and pass it as-is', async () => { + const contributes = { + configuration: { + properties: { + 'python.experiments.optInto': { + items: { + enum: ['foo', 'bar', 'All'], + }, + }, + 'python.experiments.optOutFrom': { + items: { + enum: ['foo', 'bar', 'All'], + }, + }, + }, + }, + }; + configureSettings(true, ['All'], ['All']); + configureApplicationEnvironment('stable', extensionVersion, contributes); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + await experimentService.activate(); + + const { properties } = telemetryEvents[0]; + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['All']), + optedOutFrom: JSON.stringify(['All']), }); - sinon.assert.notCalled(isCachedFlightEnabledStub); + }); + + // This is an unlikely scenario. + test('If a setting is not in package.json, set the corresponding telemetry property to an empty array', async () => { + const contributes = { + configuration: { + properties: {}, + }, + }; + configureSettings(true, ['something'], ['another']); + configureApplicationEnvironment('stable', extensionVersion, contributes); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + await experimentService.activate(); + + const { properties } = telemetryEvents[1]; + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); + }); + + // This is also an unlikely scenario. + test('If a setting does not have an enum of valid values, set the corresponding telemetry property to an empty array', async () => { + const contributes = { + configuration: { + properties: { + 'python.experiments.optInto': { + items: {}, + }, + 'python.experiments.optOutFrom': { + items: { + enum: ['foo', 'bar', 'All'], + }, + }, + }, + }, + }; + configureSettings(true, ['something'], []); + configureApplicationEnvironment('stable', extensionVersion, contributes); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + await experimentService.activate(); + + const { properties } = telemetryEvents[1]; + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); }); }); diff --git a/src/test/common/experiments/telemetry.unit.test.ts b/src/test/common/experiments/telemetry.unit.test.ts index d49ba5599c71..4c28e2ff4748 100644 --- a/src/test/common/experiments/telemetry.unit.test.ts +++ b/src/test/common/experiments/telemetry.unit.test.ts @@ -18,12 +18,14 @@ suite('Experimentation telemetry', () => { let eventProperties: Map<string, string>; setup(() => { - sendTelemetryEventStub = sinon - .stub(Telemetry, 'sendTelemetryEvent') - .callsFake((eventName: string, _, properties: object) => { - const telemetry = { eventName, properties }; - telemetryEvents.push(telemetry); - }); + sendTelemetryEventStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake((( + eventName: string, + _, + properties: object, + ) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }) as typeof Telemetry.sendTelemetryEvent); setSharedPropertyStub = sinon.stub(Telemetry, 'setSharedProperty'); eventProperties = new Map<string, string>(); @@ -42,13 +44,13 @@ suite('Experimentation telemetry', () => { experimentTelemetry.postEvent(event, eventProperties); sinon.assert.calledOnce(sendTelemetryEventStub); - assert.equal(telemetryEvents.length, 1); + assert.strictEqual(telemetryEvents.length, 1); assert.deepEqual(telemetryEvents[0], { eventName: event, properties: { foo: 'one', - bar: 'two' - } + bar: 'two', + }, }); }); diff --git a/src/test/common/extensions.unit.test.ts b/src/test/common/extensions.unit.test.ts index dcd392dbd695..75d48024b2e8 100644 --- a/src/test/common/extensions.unit.test.ts +++ b/src/test/common/extensions.unit.test.ts @@ -1,47 +1,67 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import '../../client/common/extensions'; +import { asyncFilter } from '../../client/common/utils/arrayUtils'; // 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(''); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(''); }); test('Should quote an empty space', () => { const argTotest = ' '; - expect(argTotest.toCommandArgument()).to.be.equal('" "'); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal('" "'); }); test('Should not quote command arguments without spaces', () => { const argTotest = 'one.two.three'; - expect(argTotest.toCommandArgument()).to.be.equal(argTotest); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(argTotest); }); test('Should quote command arguments with spaces', () => { const argTotest = 'one two three'; - expect(argTotest.toCommandArgument()).to.be.equal(`"${argTotest}"`); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(`"${argTotest}"`); + }); + test('Should quote file paths containing one of the parentheses: ( ', () => { + const fileToTest = 'user/code(1.py'; + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest}"`); + }); + + test('Should quote file paths containing one of the parentheses: ) ', () => { + const fileToTest = 'user)/code1.py'; + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest}"`); + }); + + test('Should quote file paths containing both of the parentheses: () ', () => { + const fileToTest = '(user)/code1.py'; + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest}"`); + }); + + test('Should quote command arguments containing ampersand', () => { + const argTotest = 'one&twothree'; + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(`"${argTotest}"`); }); test('Should return empty string for empty path', () => { const fileToTest = ''; - expect(fileToTest.fileToCommandArgument()).to.be.equal(''); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(''); }); test('Should not quote file argument without spaces', () => { const fileToTest = 'users/test/one'; - expect(fileToTest.fileToCommandArgument()).to.be.equal(fileToTest); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(fileToTest); }); test('Should quote file argument with spaces', () => { const fileToTest = 'one two three'; - expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest}"`); + expect(fileToTest.fileToCommandArgumentForPythonExt()).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, '/')); + expect(fileToTest.fileToCommandArgumentForPythonExt()).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, '/')}"`); + expect(fileToTest.fileToCommandArgumentForPythonExt()).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, '/')}"`); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); }); test('Should leave string unchanged', () => { expect('something {0}'.format()).to.be.equal('something {0}'); @@ -72,7 +92,6 @@ suite('String Extensions', () => { 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`; @@ -84,3 +103,13 @@ suite('String Extensions', () => { expect(quotedString4.trimQuotes()).to.be.equal(expectedString); }); }); + +suite('Array extensions', () => { + test('Async filter should filter items', async () => { + const stringArray = ['Hello', 'I', 'am', 'the', 'Python', 'extension']; + const result = await asyncFilter(stringArray, async (s: string) => { + return s.length > 4; + }); + assert.deepEqual(result, ['Hello', 'Python', 'extension']); + }); +}); diff --git a/src/test/common/featureDeprecationManager.unit.test.ts b/src/test/common/featureDeprecationManager.unit.test.ts deleted file mode 100644 index 2f3b48a61eb2..000000000000 --- a/src/test/common/featureDeprecationManager.unit.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// 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 index 45e14d9146a1..d8f82cdbc8e7 100644 --- a/src/test/common/helpers.test.ts +++ b/src/test/common/helpers.test.ts @@ -8,15 +8,13 @@ import { isNotInstalledError } from '../../client/common/helpers'; suite('helpers', () => { test('isNotInstalledError', (done) => { const error = new Error('something is not installed'); - assert.equal(isNotInstalledError(error), false, 'Standard error'); + assert.strictEqual(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'); + assert.strictEqual(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'); + assert.strictEqual(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 deleted file mode 100644 index 285e0df9d422..000000000000 --- a/src/test/common/insidersBuild/downloadChannelRules.unit.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -// 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 deleted file mode 100644 index f3cce07d5c56..000000000000 --- a/src/test/common/insidersBuild/downloadChannelService.unit.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -// 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 deleted file mode 100644 index df7fcf45f332..000000000000 --- a/src/test/common/insidersBuild/insidersExtensionPrompt.unit.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -// 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 deleted file mode 100644 index a0d96984e168..000000000000 --- a/src/test/common/insidersBuild/insidersExtensionService.unit.test.ts +++ /dev/null @@ -1,567 +0,0 @@ -// 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 deleted file mode 100644 index 04dd3f713b2a..000000000000 --- a/src/test/common/installer.test.ts +++ /dev/null @@ -1,407 +0,0 @@ -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 index 9bdbf88df5e8..9789f9f18718 100644 --- a/src/test/common/installer/channelManager.unit.test.ts +++ b/src/test/common/installer/channelManager.unit.test.ts @@ -13,15 +13,14 @@ 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 { InterpreterType } from '../../../client/pythonEnvironments/info'; +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; @@ -39,16 +38,13 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { 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); + moduleInstaller.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'); @@ -60,8 +56,8 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { 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); + + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); expect(channel).to.equal(undefined, 'should be undefined'); assert.ok(showNoInstallersMessage.calledOnceWith(resource)); }); @@ -69,16 +65,10 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { 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); + moduleInstaller1.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); + moduleInstaller2.setup((m) => (m as any).then).returns(() => undefined); appShell .setup((a) => a.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) @@ -88,8 +78,8 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { 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); + + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.equal(undefined, 'Channel should not be set'); @@ -98,20 +88,14 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { 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); + moduleInstaller1.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); + moduleInstaller2.setup((m) => (m as any).then).returns(() => undefined); const selection = { label: 'some label', description: '', - installer: moduleInstaller2.object + installer: moduleInstaller2.object, }; appShell .setup((a) => a.showQuickPick<typeof selection>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -122,8 +106,8 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { 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); + + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.not.equal(undefined, 'Channel should be set'); @@ -152,10 +136,7 @@ suite('InstallationChannelManager - getInstallationChannels()', () => { // 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 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); @@ -163,10 +144,7 @@ suite('InstallationChannelManager - getInstallationChannels()', () => { // 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 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); @@ -174,10 +152,7 @@ suite('InstallationChannelManager - getInstallationChannels()', () => { // 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 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); @@ -193,7 +168,6 @@ suite('InstallationChannelManager - getInstallationChannels()', () => { }); }); -// tslint:disable-next-line: max-func-body-length suite('InstallationChannelManager - showNoInstallersMessage()', () => { let interpreterService: TypeMoq.IMock<IInterpreterService>; let serviceContainer: TypeMoq.IMock<IServiceContainer>; @@ -218,7 +192,7 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { test('If active interpreter is Conda, show conda prompt', async () => { const activeInterpreter = { - type: InterpreterType.Conda + envType: EnvironmentType.Conda, }; const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); serviceContainer @@ -230,10 +204,10 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { .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())) + .setup((a) => a.showErrorMessage(Installer.noCondaOrPipInstaller, Installer.searchForHelp)) .verifiable(TypeMoq.Times.once()); installChannelManager = new InstallationChannelManager(serviceContainer.object); await installChannelManager.showNoInstallersMessage(resource); @@ -243,7 +217,7 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { test('If active interpreter is not Conda, show pip prompt', async () => { const activeInterpreter = { - type: InterpreterType.Pipenv + envType: EnvironmentType.Pipenv, }; const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); serviceContainer @@ -255,10 +229,10 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { .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())) + .setup((a) => a.showErrorMessage(Installer.noPipInstaller, Installer.searchForHelp)) .verifiable(TypeMoq.Times.once()); installChannelManager = new InstallationChannelManager(serviceContainer.object); await installChannelManager.showNoInstallersMessage(resource); @@ -266,34 +240,34 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { appShell.verifyAll(); }); - [InterpreterType.Conda, InterpreterType.Pipenv].forEach((interpreterType) => { + [EnvironmentType.Conda, EnvironmentType.Pipenv].forEach((interpreterType) => { [ { osName: 'Windows', isWindows: true, - isMac: false + isMac: false, }, { osName: 'Linux', isWindows: false, - isMac: false + isMac: false, }, { osName: 'MacOS', isWindows: false, - isMac: true - } + isMac: true, + }, ].forEach((testParams) => { const expectedURL = `https://www.bing.com/search?q=Install Pip ${testParams.osName} ${ - interpreterType === InterpreterType.Conda ? 'Conda' : '' + 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 === InterpreterType.Conda ? 'Conda' : 'not Conda' + interpreterType === EnvironmentType.Conda ? 'Conda' : 'not Conda' }`, async () => { const activeInterpreter = { - type: interpreterType + envType: interpreterType, }; const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); const platformService = TypeMoq.Mock.ofType<IPlatformService>(); @@ -310,13 +284,13 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { .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())) + .setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), Installer.searchForHelp)) + .returns(() => Promise.resolve(Installer.searchForHelp)) .verifiable(TypeMoq.Times.once()); appShell .setup((a) => a.openUrl(expectedURL)) @@ -331,7 +305,7 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { }); test("If 'Search for help' is not selected in error prompt, don't open URL", async () => { const activeInterpreter = { - type: InterpreterType.Conda + envType: EnvironmentType.Conda, }; const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); const platformService = TypeMoq.Mock.ofType<IPlatformService>(); @@ -348,11 +322,11 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { .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())) + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), Installer.searchForHelp)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); appShell diff --git a/src/test/common/installer/condaInstaller.unit.test.ts b/src/test/common/installer/condaInstaller.unit.test.ts index 28f955e03a7d..64a4a35539e4 100644 --- a/src/test/common/installer/condaInstaller.unit.test.ts +++ b/src/test/common/installer/condaInstaller.unit.test.ts @@ -11,20 +11,19 @@ import { ConfigurationService } from '../../../client/common/configuration/servi 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 { ICondaService, IComponentAdapter } 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'; +import { CondaEnvironmentInfo } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { CondaService } from '../../../client/pythonEnvironments/common/environmentManagers/condaService'; -// tslint:disable-next-line: max-func-body-length suite('Common - Conda Installer', () => { let installer: CondaInstallerTest; let serviceContainer: IServiceContainer; let condaService: ICondaService; + let condaLocatorService: IComponentAdapter; 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); } @@ -32,15 +31,17 @@ suite('Common - Conda Installer', () => { setup(() => { serviceContainer = mock(ServiceContainer); condaService = mock(CondaService); + condaLocatorService = mock<IComponentAdapter>(); configService = mock(ConfigurationService); when(serviceContainer.get<ICondaService>(ICondaService)).thenReturn(instance(condaService)); + when(serviceContainer.get<IComponentAdapter>(IComponentAdapter)).thenReturn(instance(condaLocatorService)); 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); + assert.strictEqual(installer.displayName, 'Conda'); + assert.strictEqual(installer.name, 'Conda'); + assert.strictEqual(installer.priority, 10); }); test('Installer is not supported when conda is available variable is set to false', async () => { const uri = Uri.file(__filename); @@ -48,7 +49,7 @@ suite('Common - Conda Installer', () => { const supported = await installer.isSupported(uri); - assert.equal(supported, false); + assert.strictEqual(supported, false); }); test('Installer is not supported when conda is not available', async () => { const uri = Uri.file(__filename); @@ -56,7 +57,7 @@ suite('Common - Conda Installer', () => { const supported = await installer.isSupported(uri); - assert.equal(supported, false); + assert.strictEqual(supported, false); }); test('Installer is not supported when current env is not a conda env', async () => { const uri = Uri.file(__filename); @@ -66,11 +67,11 @@ suite('Common - Conda Installer', () => { when(settings.pythonPath).thenReturn(pythonPath); when(condaService.isCondaAvailable()).thenResolve(true); when(configService.getSettings(uri)).thenReturn(instance(settings)); - when(condaService.isCondaEnvironment(pythonPath)).thenResolve(false); + when(condaLocatorService.isCondaEnvironment(pythonPath)).thenResolve(false); const supported = await installer.isSupported(uri); - assert.equal(supported, false); + assert.strictEqual(supported, false); }); test('Installer is supported when current env is a conda env', async () => { const uri = Uri.file(__filename); @@ -80,11 +81,11 @@ suite('Common - Conda Installer', () => { when(settings.pythonPath).thenReturn(pythonPath); when(condaService.isCondaAvailable()).thenResolve(true); when(configService.getSettings(uri)).thenReturn(instance(settings)); - when(condaService.isCondaEnvironment(pythonPath)).thenResolve(true); + when(condaLocatorService.isCondaEnvironment(pythonPath)).thenResolve(true); const supported = await installer.isSupported(uri); - assert.equal(supported, true); + assert.strictEqual(supported, true); }); test('Include name of environment', async () => { const uri = Uri.file(__filename); @@ -93,17 +94,21 @@ suite('Common - Conda Installer', () => { const condaPath = 'some Conda Path'; const condaEnv: CondaEnvironmentInfo = { name: 'Hello', - path: 'Some Path' + 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); + when(condaService.getCondaFile(true)).thenResolve(condaPath); + when(condaLocatorService.getCondaEnvironment(pythonPath)).thenResolve(condaEnv); const execInfo = await installer.getExecutionInfo('abc', uri); - assert.deepEqual(execInfo, { args: ['install', '--name', condaEnv.name, 'abc', '-y'], execPath: condaPath }); + assert.deepEqual(execInfo, { + args: ['install', '--name', condaEnv.name, 'abc', '-y'], + execPath: condaPath, + useShell: true, + }); }); test('Include path of environment', async () => { const uri = Uri.file(__filename); @@ -112,19 +117,20 @@ suite('Common - Conda Installer', () => { const condaPath = 'some Conda Path'; const condaEnv: CondaEnvironmentInfo = { name: '', - path: 'Some Path' + 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); + when(condaService.getCondaFile(true)).thenResolve(condaPath); + when(condaLocatorService.getCondaEnvironment(pythonPath)).thenResolve(condaEnv); const execInfo = await installer.getExecutionInfo('abc', uri); assert.deepEqual(execInfo, { - args: ['install', '--prefix', condaEnv.path.fileToCommandArgument(), 'abc', '-y'], - execPath: condaPath + args: ['install', '--prefix', condaEnv.path.fileToCommandArgumentForPythonExt(), 'abc', '-y'], + execPath: condaPath, + useShell: true, }); }); }); diff --git a/src/test/common/installer/extensionBuildInstaller.unit.test.ts b/src/test/common/installer/extensionBuildInstaller.unit.test.ts deleted file mode 100644 index 13676a9658f8..000000000000 --- a/src/test/common/installer/extensionBuildInstaller.unit.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -// 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 deleted file mode 100644 index b7484c27c0e3..000000000000 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -// 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 { PythonInterpreter } 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<PythonInterpreter>(); - // 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 deleted file mode 100644 index 1ea7be604f70..000000000000 --- a/src/test/common/installer/installer.unit.test.ts +++ /dev/null @@ -1,1087 +0,0 @@ -// 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 { PythonInterpreter } 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<PythonInterpreter>(); - // 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 index e11fb2d56505..3df64ceb2dec 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -1,53 +1,49 @@ +/* eslint-disable class-methods-use-this */ // 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 { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { - CancellationTokenSource, - Disposable, - OutputChannel, - ProgressLocation, - Uri, - WorkspaceConfiguration -} from 'vscode'; +import { CancellationTokenSource, Disposable, 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 { + IInstallationChannelManager, + IModuleInstaller, + ModuleInstallFlags, +} from '../../../client/common/installer/types'; import { IFileSystem } from '../../../client/common/platform/types'; +import { _SCRIPTS_DIR } from '../../../client/common/process/internal/scripts/constants'; import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; import { ExecutionInfo, IConfigurationService, IDisposableRegistry, - IOutputChannel, + IInstaller, + ILogOutputChannel, IPythonSettings, - ModuleNamePurpose, - Product + 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 { Architecture } from '../../../client/common/utils/platform'; +import { IComponentAdapter, ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; -import { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; -import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import * as logging from '../../../client/logging'; +import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; -const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py'); +const pythonPath = path.join(__dirname, 'python'); /* Complex test to ensure we cover all combinations: We could have written separate tests for each installer, but we'd be replicate code. @@ -67,38 +63,49 @@ suite('Module Installer', () => { public get priority(): number { return 0; } + public get name(): string { return ''; } + public get displayName(): string { return ''; } - public isSupported(_resource?: InterpreterUri): Promise<boolean> { + + public get type(): ModuleInstallerType { + return ModuleInstallerType.Unknown; + } + + public isSupported(): Promise<boolean> { return Promise.resolve(false); } - public getExecutionInfo(_moduleName: string, _resource?: InterpreterUri): Promise<ExecutionInfo> { + + public getExecutionInfo(): 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 outputChannel: TypeMoq.IMock<ILogOutputChannel>; + let appShell: TypeMoq.IMock<IApplicationShell>; let serviceContainer: TypeMoq.IMock<IServiceContainer>; - const pythonPath = path.join(__dirname, 'python'); suite('Method _elevatedInstall()', async () => { + let traceLogStub: sinon.SinonStub; let installer: TestModuleInstaller; const execPath = 'execPath'; const args = ['1', '2']; const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`; setup(() => { + traceLogStub = sinon.stub(logging, 'traceLog'); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - outputChannel = TypeMoq.Mock.ofType<IOutputChannel>(); + outputChannel = TypeMoq.Mock.ofType<ILogOutputChannel>(); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) + .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel.object); appShell = TypeMoq.Mock.ofType<IApplicationShell>(); serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); @@ -106,12 +113,15 @@ suite('Module Installer', () => { }); teardown(() => { rewiremock.disable(); + sinon.restore(); }); 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') + // eslint-disable-next-line @typescript-eslint/ban-types + exec: (_command: unknown, _options: unknown, callBackFn: Function) => + callBackFn(error, 'stdout', 'stderr'), }; rewiremock.enable(); rewiremock('sudo-prompt').with(sudoPromptMock); @@ -119,20 +129,17 @@ suite('Module Installer', () => { .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(); + traceLogStub.calledOnceWithExactly(`[Elevated] ${command}`); }); test('Show stdout if sudo exec succeeds', async () => { const stdout = 'stdout'; const sudoPromptMock = { - exec: (_command: any, _options: any, callBackFn: Function) => callBackFn(undefined, stdout, undefined) + // eslint-disable-next-line @typescript-eslint/ban-types + exec: (_command: unknown, _options: unknown, callBackFn: Function) => + callBackFn(undefined, stdout, undefined), }; rewiremock.enable(); rewiremock('sudo-prompt').with(sudoPromptMock); @@ -140,69 +147,55 @@ suite('Module Installer', () => { .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(); + traceLogStub.calledOnceWithExactly(`[Elevated] ${command}`); }); 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) + // eslint-disable-next-line @typescript-eslint/ban-types + exec: (_command: unknown, _options: unknown, 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(); + traceLogStub.calledOnceWithExactly(`[Elevated] ${command}`); + traceLogStub.calledOnceWithExactly(`Warning: ${stderr}`); }); }); - [CondaInstaller, PipInstaller, PipEnvInstaller, TestModuleInstaller].forEach((installerClass) => { + [CondaInstaller, PipInstaller, PipEnvInstaller, TestModuleInstaller].forEach((InstallerClass) => { // Proxy info is relevant only for PipInstaller. - const proxyServers = installerClass === PipInstaller ? ['', 'proxy:1234'] : ['']; + 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 + 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') } + { name: '', path: path.join('conda with spaces', 'path') }, ] : []; [undefined, ...condaEnvs].forEach((condaEnvInfo) => { const testProxySuffix = proxyServer.length === 0 ? 'without proxy info' : 'with proxy info'; + // eslint-disable-next-line no-nested-ternary 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})`, () => { + suite(`${InstallerClass.name} (${testSuite})`, () => { let disposables: Disposable[] = []; let installationChannel: TypeMoq.IMock<IInstallationChannelManager>; let terminalService: TypeMoq.IMock<ITerminalService>; @@ -233,13 +226,21 @@ suite('Module Installer', () => { installationChannel = TypeMoq.Mock.ofType<IInstallationChannelManager>(); serviceContainer .setup((c) => - c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny()) + 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.getCondaFile(true)) + .returns(() => Promise.resolve(condaExecutable)); + + const condaLocatorService = TypeMoq.Mock.ofType<IComponentAdapter>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IComponentAdapter))) + .returns(() => condaLocatorService.object); + condaLocatorService .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) .returns(() => Promise.resolve(condaEnvInfo)); @@ -256,7 +257,7 @@ suite('Module Installer', () => { terminalService = TypeMoq.Mock.ofType<ITerminalService>(); const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); terminalServiceFactory - .setup((f) => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) .returns(() => terminalService.object); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(ITerminalServiceFactory), TypeMoq.It.isAny())) @@ -276,13 +277,12 @@ suite('Module Installer', () => { .returns(() => workspaceService.object); const http = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); http.setup((h) => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns( - () => proxyServer + () => proxyServer, ); workspaceService .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))) .returns(() => http.object); - - installer = new installerClass(serviceContainer.object); + installer = new InstallerClass(serviceContainer.object); }); teardown(() => { disposables.forEach((disposable) => { @@ -292,293 +292,295 @@ suite('Module Installer', () => { }); sinon.restore(); }); - function setActiveInterpreter(activeInterpreter?: PythonInterpreter) { + function setActiveInterpreter(activeInterpreter?: PythonEnvironment) { interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .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) + getModuleNamesForTesting() + .filter((item) => item.value !== Product.ensurepip) + .forEach((product) => { + const { moduleName } = product; + async function installModuleAndVerifyCommand( + command: string, + expectedArgs: string[], + flags?: ModuleInstallFlags, + ) { + 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<PythonInterpreter>(); - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.type).returns(() => InterpreterType.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(); + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await installer.installModule(product.value, resource, undefined, flags); + terminalService.verifyAll(); + } + + 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>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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')); + info.setup((t) => t.path).returns(() => pythonPath); + 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.value, resource); + } catch (ex) { + noop(); + } + const args = ['-m', '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>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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')); + info.setup((t) => t.path).returns(() => pythonPath); + setActiveInterpreter(info.object); + pythonSettings.setup((p) => p.globalModuleInstallation).returns(() => true); + fs.setup((f) => f.isDirReadonly(path.dirname(pythonPath))).returns(() => + Promise.resolve(false), + ); + const args = ['-m', 'executionInfo']; + terminalService + .setup((t) => t.sendCommand(pythonPath, args, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + try { + await installer.installModule(product.value, 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>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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')); + info.setup((t) => t.path).returns(() => pythonPath); + setActiveInterpreter(info.object); + pythonSettings + .setup((p) => p.globalModuleInstallation) + .returns(() => false); + const args = + product.value === Product.pip + ? ['-m', 'executionInfo'] // Pipe is always installed into the environment. + : ['-m', 'executionInfo', '--user']; + terminalService + .setup((t) => t.sendCommand(pythonPath, args, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + try { + await installer.installModule(product.value, resource); + } catch (ex) { + noop(); + } + interpreterService.verifyAll(); + terminalService.verifyAll(); + }); + test(`ignores failures in IFileSystem.isDirReadonly()`, async () => { + const info = TypeMoq.Mock.ofType<PythonEnvironment>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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')); + info.setup((t) => t.path).returns(() => pythonPath); + 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.value, resource); + } catch (ex) { + noop(); + } + const args = ['-m', '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: `Installing ${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.value, + resource, + new CancellationTokenSource().token, + ); + } catch (ex) { + noop(); + } + interpreterService.verifyAll(); + appShell.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<PythonInterpreter>(); - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.type).returns(() => InterpreterType.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()); + } + + if (InstallerClass === PipInstaller) { + test(`Ensure getActiveInterpreter is used in PipInstaller (${product.name})`, async () => { + if (product.value === Product.pip) { + const mockInstaller = mock<IInstaller>(); + serviceContainer + .setup((svc) => svc.get<IInstaller>(TypeMoq.It.isValue(IInstaller))) + .returns(() => instance(mockInstaller)); + when(mockInstaller.isInstalled(Product.ensurepip, anything())).thenResolve( + true, + ); + } + setActiveInterpreter(); try { - await installer.installModule(product.name, resource); - } catch (ex) { + await installer.installModule(product.value, resource); + } catch { 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<PythonInterpreter>(); - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.type).returns(() => InterpreterType.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(); + test(`Test Args (${product.name})`, async () => { + if (product.value === Product.pip) { + const mockInstaller = mock<IInstaller>(); + serviceContainer + .setup((svc) => svc.get<IInstaller>(TypeMoq.It.isValue(IInstaller))) + .returns(() => instance(mockInstaller)); + when(mockInstaller.isInstalled(Product.pip, anything())).thenResolve(true); + when(mockInstaller.isInstalled(Product.ensurepip, anything())).thenResolve( + true, + ); } + setActiveInterpreter(); + const proxyArgs = proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; + const expectedArgs = + product.value === Product.pip + ? ['-m', 'ensurepip'] + : ['-m', 'pip', ...proxyArgs, 'install', '-U', moduleName]; + console.log(`Expected: ${expectedArgs.join(' ')}`); + await installModuleAndVerifyCommand(pythonPath, expectedArgs); interpreterService.verifyAll(); - terminalService.verifyAll(); }); - test(`ignores failures in IFileSystem.isDirReadonly()`, async () => { - const info = TypeMoq.Mock.ofType<PythonInterpreter>(); - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.type).returns(() => InterpreterType.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) - ); + if (product.value === Product.pip) { + test(`Test Args (${product.name}) if ensurepip is not available`, async () => { + if (product.value === Product.pip) { + const mockInstaller = mock<IInstaller>(); + serviceContainer + .setup((svc) => svc.get<IInstaller>(TypeMoq.It.isValue(IInstaller))) + .returns(() => instance(mockInstaller)); + when(mockInstaller.isInstalled(Product.pip, anything())).thenResolve( + false, + ); + when( + mockInstaller.isInstalled(Product.ensurepip, anything()), + ).thenResolve(false); + } + const interpreterInfo = { + architecture: Architecture.Unknown, + envType: EnvironmentType.Unknown, + path: pythonPath, + sysPrefix: '', + }; + setActiveInterpreter(interpreterInfo); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(interpreterInfo)); + const expectedArgs = [path.join(_SCRIPTS_DIR, 'get-pip.py')]; - try { - await installer.installModule(product.name, resource); - } catch (ex) { - noop(); - } - const args = [isolated, 'executionInfo']; - assert.ok(elevatedInstall.calledOnceWith(pythonPath, args)); - interpreterService.verifyAll(); + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + interpreterService.verifyAll(); + }); + } + } + if (InstallerClass === PipEnvInstaller) { + [false, true].forEach((isUpgrade) => { + test(`Test args (${product.name})`, async () => { + setActiveInterpreter(); + const expectedArgs = [ + isUpgrade ? 'update' : 'install', + moduleName, + '--dev', + ]; + await installModuleAndVerifyCommand( + pipenvName, + expectedArgs, + isUpgrade ? ModuleInstallFlags.upgrade : undefined, + ); + }); }); - 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 + } + if (InstallerClass === CondaInstaller) { + [false, true].forEach((isUpgrade) => { + test(`Test args (${product.name})`, async () => { + setActiveInterpreter(); + const expectedArgs = [isUpgrade ? 'update' : 'install']; + if ( + [ + 'pandas', + 'tensorboard', + 'ipykernel', + 'jupyter', + 'notebook', + 'nbconvert', + ].includes(product.name) + ) { + expectedArgs.push('-c', 'conda-forge'); + } + if (condaEnvInfo && condaEnvInfo.name) { + expectedArgs.push('--name'); + expectedArgs.push(condaEnvInfo.name.toCommandArgumentForPythonExt()); + } else if (condaEnvInfo && condaEnvInfo.path) { + expectedArgs.push('--prefix'); + expectedArgs.push( + condaEnvInfo.path.fileToCommandArgumentForPythonExt(), + ); + } + expectedArgs.push(moduleName); + expectedArgs.push('-y'); + await installModuleAndVerifyCommand( + condaExecutable, + expectedArgs, + isUpgrade ? ModuleInstallFlags.upgrade : undefined, ); - } 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); - }); - } - }); + } + }); }); }); }); @@ -586,31 +588,17 @@ suite('Module Installer', () => { }); }); -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<PythonInterpreter>(); - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.type).returns(() => InterpreterType.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); + const prodInstaller = new ProductInstaller(mockSvc); + moduleName = prodInstaller.translateProductToModuleName(product.value); return { name: product.name, value: product.value, moduleName }; } catch { - return; + return undefined; } }) .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 index b76f18add910..25b1b910daaa 100644 --- a/src/test/common/installer/pipEnvInstaller.unit.test.ts +++ b/src/test/common/installer/pipEnvInstaller.unit.test.ts @@ -4,27 +4,47 @@ 'use strict'; import { expect } from 'chai'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; -import { IInterpreterLocatorService, PIPENV_SERVICE } from '../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; -import { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; +import * as pipEnvHelper from '../../../client/pythonEnvironments/common/environmentManagers/pipenv'; +import { EnvironmentType } 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 isPipenvEnvironmentRelatedToFolder: sinon.SinonStub; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; let pipEnvInstaller: PipEnvInstaller; + const interpreterPath = 'path/to/interpreter'; + const workspaceFolder = 'path/to/folder'; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - locatorService = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(PIPENV_SERVICE))) - .returns(() => locatorService.object); + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + + isPipenvEnvironmentRelatedToFolder = sinon + .stub(pipEnvHelper, 'isPipenvEnvironmentRelatedToFolder') + .callsFake((interpreter: string, folder: string) => { + return Promise.resolve(interpreterPath === interpreter && folder === workspaceFolder); + }); pipEnvInstaller = new PipEnvInstaller(serviceContainer.object); }); + teardown(() => { + isPipenvEnvironmentRelatedToFolder.restore(); + }); + test('Installer name is pipenv', () => { expect(pipEnvInstaller.name).to.equal('pipenv'); }); @@ -35,39 +55,58 @@ suite('PipEnv installer', async () => { test('If InterpreterUri is Pipenv interpreter, method isSupported() returns true', async () => { const interpreter = { - type: InterpreterType.Pipenv + 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 = { - type: InterpreterType.Conda + 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 () => { + test('If active environment is pipenv and is related to workspace folder, return true', async () => { const resource = Uri.parse('a'); - locatorService - .setup((p) => p.getInterpreters(resource)) - .returns(() => - Promise.resolve([ - TypeMoq.Mock.ofType<PythonInterpreter>().object, - TypeMoq.Mock.ofType<PythonInterpreter>().object - ]) - ); + + interpreterService + .setup((p) => p.getActiveInterpreter(resource)) + .returns(() => Promise.resolve({ envType: EnvironmentType.Pipenv, path: interpreterPath } as any)); + + workspaceService + .setup((w) => w.getWorkspaceFolder(resource)) + .returns(() => ({ uri: { fsPath: workspaceFolder } } as any)); 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 () => { + test('If active environment is not pipenv, return false', async () => { const resource = Uri.parse('a'); - locatorService.setup((p) => p.getInterpreters(resource)).returns(() => Promise.resolve([])); + interpreterService + .setup((p) => p.getActiveInterpreter(resource)) + .returns(() => Promise.resolve({ envType: EnvironmentType.Conda, path: interpreterPath } as any)); + + workspaceService + .setup((w) => w.getWorkspaceFolder(resource)) + .returns(() => ({ uri: { fsPath: workspaceFolder } } as any)); + const result = await pipEnvInstaller.isSupported(resource); + expect(result).to.equal(false, 'Should be false'); + }); + + test('If active environment is pipenv but not related to workspace folder, return false', async () => { + const resource = Uri.parse('a'); + interpreterService + .setup((p) => p.getActiveInterpreter(resource)) + .returns(() => Promise.resolve({ envType: EnvironmentType.Pipenv, path: 'some random path' } as any)); + + workspaceService + .setup((w) => w.getWorkspaceFolder(resource)) + .returns(() => ({ uri: { fsPath: workspaceFolder } } as any)); 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 index de27c0b4e938..7b7af714f7f7 100644 --- a/src/test/common/installer/pipInstaller.unit.test.ts +++ b/src/test/common/installer/pipInstaller.unit.test.ts @@ -8,16 +8,29 @@ 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 { IInterpreterService } 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('Pip installer', async () => { +suite('xPip installer', async () => { let serviceContainer: TypeMoq.IMock<IServiceContainer>; let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; let pipInstaller: PipInstaller; + const interpreter = { + path: 'pythonPath', + envType: EnvironmentType.System, + }; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve((interpreter as unknown) as PythonEnvironment)); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) .returns(() => pythonExecutionFactory.object); @@ -34,9 +47,6 @@ suite('Pip installer', async () => { 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) => { @@ -44,12 +54,8 @@ suite('Pip installer', async () => { }) .returns(() => Promise.resolve(pythonExecutionService.object)) .verifiable(TypeMoq.Times.once()); - pythonExecutionService - // tslint:disable-next-line: no-any - .setup((p) => (p as any).then) - .returns(() => undefined); + pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); - // tslint:disable-next-line: no-any await pipInstaller.isSupported(interpreter as any); pythonExecutionFactory.verifyAll(); @@ -65,26 +71,35 @@ suite('Pip installer', async () => { }) .returns(() => Promise.resolve(pythonExecutionService.object)) .verifiable(TypeMoq.Times.once()); - pythonExecutionService - // tslint:disable-next-line: no-any - .setup((p) => (p as any).then) - .returns(() => undefined); + pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); await pipInstaller.isSupported(resource); pythonExecutionFactory.verifyAll(); }); + test('If InterpreterUri is Resource and active environment is conda without python, pip installer is not supported', async () => { + const resource = Uri.parse('a'); + const condaInterpreter = { + path: 'path/to/python', + envType: EnvironmentType.Conda, + envPath: 'path/to/enviornment', + }; + interpreterService.reset(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve((condaInterpreter as unknown) as PythonEnvironment)); + const result = await pipInstaller.isSupported(resource); + expect(result).to.equal(false); + }); + 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 as any).then).returns(() => undefined); pythonExecutionService.setup((p) => p.isModuleInstalled('pip')).returns(() => Promise.resolve(true)); const expected = await pipInstaller.isSupported(resource); @@ -98,10 +113,7 @@ suite('Pip installer', async () => { 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 as any).then).returns(() => undefined); pythonExecutionService.setup((p) => p.isModuleInstalled('pip')).returns(() => Promise.resolve(false)); const expected = await pipInstaller.isSupported(resource); @@ -115,10 +127,7 @@ suite('Pip installer', async () => { 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 as any).then).returns(() => undefined); pythonExecutionService .setup((p) => p.isModuleInstalled('pip')) .returns(() => Promise.reject('Unable to check if module is installed')); diff --git a/src/test/common/installer/poetryInstaller.unit.test.ts b/src/test/common/installer/poetryInstaller.unit.test.ts index 2f72b40e97dc..07d60159138e 100644 --- a/src/test/common/installer/poetryInstaller.unit.test.ts +++ b/src/test/common/installer/poetryInstaller.unit.test.ts @@ -3,6 +3,8 @@ 'use strict'; +import * as sinon from 'sinon'; +import * as path from 'path'; import * as assert from 'assert'; import { expect } from 'chai'; import { anything, instance, mock, when } from 'ts-mockito'; @@ -12,43 +14,65 @@ 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 { ExecutionResult, ShellOptions } from '../../../client/common/process/types'; import { ExecutionInfo, IConfigurationService } from '../../../client/common/types'; import { ServiceContainer } from '../../../client/ioc/container'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { TEST_LAYOUT_ROOT } from '../../pythonEnvironments/common/commonTestConstants'; +import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; -// 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); } } + const testPoetryDir = path.join(TEST_LAYOUT_ROOT, 'poetry'); + const project1 = path.join(testPoetryDir, 'project1'); let poetryInstaller: TestInstaller; let workspaceService: IWorkspaceService; let configurationService: IConfigurationService; - let fileSystem: IFileSystem; - let processServiceFactory: IProcessServiceFactory; + let interpreterService: IInterpreterService; + let serviceContainer: ServiceContainer; + let shellExecute: sinon.SinonStub; + setup(() => { - const serviceContainer = mock(ServiceContainer); + serviceContainer = mock(ServiceContainer); + interpreterService = mock<IInterpreterService>(); + when(serviceContainer.get<IInterpreterService>(IInterpreterService)).thenReturn(instance(interpreterService)); workspaceService = mock(WorkspaceService); configurationService = mock(ConfigurationService); - fileSystem = mock(FileSystem); - processServiceFactory = mock(ProcessServiceFactory); + + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + shellExecute.callsFake((command: string, options: ShellOptions) => { + // eslint-disable-next-line default-case + switch (command) { + case 'poetry env list --full-path': + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + case 'poetry env info -p': { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project1)) { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `${path.join(project1, '.venv')} \n`, + }); + } + } + } + return Promise.reject(new Error('Command failed')); + }); poetryInstaller = new TestInstaller( instance(serviceContainer), instance(workspaceService), instance(configurationService), - instance(fileSystem), - instance(processServiceFactory) ); }); + teardown(() => { + shellExecute?.restore(); + }); + test('Installer name is poetry', () => { expect(poetryInstaller.name).to.equal('poetry'); }); @@ -63,92 +87,105 @@ suite('Module Installer - Poetry', () => { test('Is not supported when there is no resource', async () => { const supported = await poetryInstaller.isSupported(); - assert.equal(supported, false); + assert.strictEqual(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); + assert.strictEqual(supported, false); }); - test('Is not supported when the poetry file does not exists', async () => { + test('Get Executable info', async () => { const uri = Uri.file(__dirname); - when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - when(fileSystem.fileExists(anything())).thenResolve(false); + const settings = mock(PythonSettings); - const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); + + const info = await poetryInstaller.getExecutionInfo('something', uri); - assert.equal(supported, false); + assert.deepEqual(info, { args: ['add', '--group', 'dev', 'something'], execPath: 'poetry path' }); }); - test('Is not supported when the poetry is not available (with stderr)', async () => { + test('Get executable info when installing black', 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: '' }); + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); - const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + const info = await poetryInstaller.getExecutionInfo('black', uri); - assert.equal(supported, false); + assert.deepEqual(info, { + args: ['add', '--group', 'dev', 'black'], + execPath: 'poetry path', + }); }); - test('Is not supported when the poetry is not available (with error running poetry)', async () => { - const uri = Uri.file(__dirname); - const processService = mock(ProcessService); + test('Is supported returns true if selected interpreter is related to the workspace', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: path.join(project1, '.venv', 'Scripts', 'python.exe'), + envType: EnvironmentType.Poetry, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); 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); + assert.strictEqual(supported, true); }); - test('Is supported', async () => { - const uri = Uri.file(__dirname); - const processService = mock(ProcessService); + + test('Is supported returns true if no interpreter is selected', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); - when(configurationService.getSettings(uri)).thenReturn(instance(settings)); - when(settings.poetryPath).thenReturn('poetry path'); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(undefined); + 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(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); + assert.strictEqual(supported, false); }); - test('Get Executable info', async () => { - const uri = Uri.file(__dirname); + + test('Is supported returns false if selected interpreter is not related to the workspace', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); - when(configurationService.getSettings(uri)).thenReturn(instance(settings)); - when(settings.poetryPath).thenReturn('poetry path'); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: path.join(project1, '.random', 'Scripts', 'python.exe'), + envType: EnvironmentType.Poetry, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry'); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - const info = await poetryInstaller.getExecutionInfo('something', uri); + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); - assert.deepEqual(info, { args: ['add', '--dev', 'something'], execPath: 'poetry path' }); + assert.strictEqual(supported, false); }); - test('Get executable info when installing black', async () => { - const uri = Uri.file(__dirname); + + test('Is supported returns false if selected interpreter is not of Poetry type', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); - when(configurationService.getSettings(uri)).thenReturn(instance(settings)); - when(settings.poetryPath).thenReturn('poetry path'); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: path.join(project1, '.venv', 'Scripts', 'python.exe'), + envType: EnvironmentType.Pipenv, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry'); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - const info = await poetryInstaller.getExecutionInfo('black', uri); + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); - assert.deepEqual(info, { args: ['add', '--dev', 'black', '--allow-prereleases'], execPath: 'poetry path' }); + assert.strictEqual(supported, false); }); }); diff --git a/src/test/common/installer/productInstaller.unit.test.ts b/src/test/common/installer/productInstaller.unit.test.ts new file mode 100644 index 000000000000..2934d613f88f --- /dev/null +++ b/src/test/common/installer/productInstaller.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 { IApplicationShell } from '../../../client/common/application/types'; +import { DataScienceInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +import { InstallerResponse, Product } from '../../../client/common/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +class AlwaysInstalledDataScienceInstaller extends DataScienceInstaller { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + public async isInstalled(_product: Product, _resource?: InterpreterUri): Promise<boolean> { + return true; + } +} + +suite('DataScienceInstaller install', async () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let installationChannelManager: TypeMoq.IMock<IInstallationChannelManager>; + let dataScienceInstaller: DataScienceInstaller; + let appShell: TypeMoq.IMock<IApplicationShell>; + + const interpreterPath = 'path/to/interpreter'; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + installationChannelManager = TypeMoq.Mock.ofType<IInstallationChannelManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(undefined)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInstallationChannelManager))) + .returns(() => installationChannelManager.object); + + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + + dataScienceInstaller = new AlwaysInstalledDataScienceInstaller(serviceContainer.object); + }); + + teardown(() => { + // noop + }); + + test('Will invoke pip for pytorch with conda environment', async () => { + // See https://github.com/microsoft/vscode-jupyter/issues/5034 + const testEnvironment: PythonEnvironment = { + envType: EnvironmentType.Conda, + envName: 'test', + envPath: interpreterPath, + path: interpreterPath, + architecture: Architecture.x64, + sysPrefix: '', + }; + const testInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + + testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pip); + testInstaller + .setup((c) => + c.installModule( + TypeMoq.It.isValue(Product.torchProfilerInstallName), + TypeMoq.It.isValue(testEnvironment), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve()); + + installationChannelManager + .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([testInstaller.object])); + + const result = await dataScienceInstaller.install(Product.torchProfilerInstallName, testEnvironment); + expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); + }); +}); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts deleted file mode 100644 index fc2d8c7e2b7b..000000000000 --- a/src/test/common/installer/productPath.unit.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -// 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 index c355515fa024..8a811ad7ac4d 100644 --- a/src/test/common/installer/serviceRegistry.unit.test.ts +++ b/src/test/common/installer/serviceRegistry.unit.test.ts @@ -4,32 +4,19 @@ 'use strict'; import { instance, mock, verify } from 'ts-mockito'; -import { IWebPanelProvider } from '../../../client/common/application/types'; -import { WebPanelProvider } from '../../../client/common/application/webPanels/webPanelProvider'; 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 { 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'; @@ -51,67 +38,16 @@ suite('Common installer Service Registry', () => { verify( serviceManager.addSingleton<IInstallationChannelManager>( IInstallationChannelManager, - InstallationChannelManager - ) - ).once(); - verify( - serviceManager.addSingleton<IExtensionBuildInstaller>( - IExtensionBuildInstaller, - StableBuildInstaller, - STABLE_INSTALLER - ) + InstallationChannelManager, + ), ).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 - ) + ProductType.TestFramework, + ), ).once(); - verify(serviceManager.addSingleton<IWebPanelProvider>(IWebPanelProvider, WebPanelProvider)).once(); }); }); diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts index ad4aa91b1e4c..58a34b3cbcde 100644 --- a/src/test/common/interpreterPathService.unit.test.ts +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -12,15 +12,13 @@ import { Event, EventEmitter, Uri, - WorkspaceConfiguration + WorkspaceConfiguration, } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; +import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; import { defaultInterpreterPathSetting, + getCIPythonPath, 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'; @@ -30,6 +28,7 @@ suite('Interpreter Path Service', async () => { let interpreterPathService: InterpreterPathService; let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; const resource = Uri.parse('a'); const resourceOutsideOfWorkspace = Uri.parse('b'); const interpreterPath = 'path/to/interpreter'; @@ -37,209 +36,30 @@ suite('Interpreter Path Service', async () => { setup(() => { const event = TypeMoq.Mock.ofType<Event<ConfigurationChangeEvent>>(); workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + appEnvironment.setup((a) => a.remoteName).returns(() => undefined); workspaceService .setup((w) => w.getWorkspaceFolder(resource)) .returns(() => ({ uri: resource, name: 'Workspacefolder', - index: 0 + 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, []); + interpreterPathService = new InterpreterPathService( + persistentStateFactory.object, + workspaceService.object, + [], + appEnvironment.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); @@ -248,9 +68,8 @@ suite('Interpreter Path Service', async () => { .returns( () => ({ - globalValue: interpreterPath - // tslint:disable-next-line: no-any - } as any) + globalValue: interpreterPath, + } as any), ); workspaceConfig .setup((w) => w.update('defaultInterpreterPath', interpreterPath, true)) @@ -270,9 +89,8 @@ suite('Interpreter Path Service', async () => { .returns( () => ({ - globalValue: 'storedValue' - // tslint:disable-next-line: no-any - } as any) + globalValue: 'storedValue', + } as any), ); workspaceConfig .setup((w) => w.update('defaultInterpreterPath', interpreterPath, true)) @@ -452,15 +270,16 @@ suite('Interpreter Path Service', async () => { 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); + workspaceService + .setup((w) => w.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); workspaceConfig .setup((w) => w.inspect<string>('defaultInterpreterPath')) .returns( () => ({ - globalValue: 'default/path/to/interpreter' - // tslint:disable-next-line: no-any - } as any) + globalValue: 'default/path/to/interpreter', + } as any), ); const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); @@ -473,7 +292,7 @@ suite('Interpreter Path Service', async () => { assert.deepEqual(settings, { globalValue: 'default/path/to/interpreter', workspaceFolderValue: undefined, - workspaceValue: undefined + workspaceValue: undefined, }); persistentStateFactory.verifyAll(); @@ -485,15 +304,14 @@ suite('Interpreter Path Service', async () => { // 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); + workspaceService.setup((w) => w.getConfiguration('python', resource)).returns(() => workspaceConfig.object); workspaceConfig .setup((w) => w.inspect<string>('defaultInterpreterPath')) .returns( () => ({ - globalValue: 'default/path/to/interpreter' - // tslint:disable-next-line: no-any - } as any) + globalValue: 'default/path/to/interpreter', + } as any), ); const workspaceFolderPersistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); @@ -510,7 +328,7 @@ suite('Interpreter Path Service', async () => { assert.deepEqual(settings, { globalValue: 'default/path/to/interpreter', workspaceFolderValue: 'workspaceFolderValue', - workspaceValue: 'workspaceFolderValue' + workspaceValue: 'workspaceFolderValue', }); }); @@ -522,22 +340,21 @@ suite('Interpreter Path Service', async () => { // 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); + workspaceService.setup((w) => w.getConfiguration('python', resource)).returns(() => workspaceConfig.object); workspaceConfig .setup((w) => w.inspect<string>('defaultInterpreterPath')) .returns( () => ({ - globalValue: 'default/path/to/interpreter' - // tslint:disable-next-line: no-any - } as any) + globalValue: 'default/path/to/interpreter', + } 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) + p.createGlobalPersistentState<string | undefined>(expectedWorkspaceFolderSettingKey, undefined), ) .returns(() => workspaceFolderPersistentState.object); persistentStateFactory @@ -551,35 +368,67 @@ suite('Interpreter Path Service', async () => { assert.deepEqual(settings, { globalValue: 'default/path/to/interpreter', workspaceFolderValue: 'workspaceFolderValue', - workspaceValue: 'workspaceValue' + workspaceValue: 'workspaceValue', }); }); - test(`Getting setting value returns workspace folder value if it's defined`, async () => { - interpreterPathService.inspect = () => ({ + test('Inspecting settings falls back to default interpreter setting if no interpreter is set', 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', resource)).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter', + workspaceValue: 'defaultWorkspaceValue', + workspaceFolderValue: 'defaultWorkspaceFolderValue', + } 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(() => undefined); + workspacePersistentState.setup((p) => p.value).returns(() => undefined); + + const settings = interpreterPathService.inspect(resource); + + assert.deepEqual(settings, { globalValue: 'default/path/to/interpreter', - workspaceFolderValue: 'workspaceFolderValue', - workspaceValue: 'workspaceValue' + workspaceFolderValue: 'defaultWorkspaceFolderValue', + workspaceValue: 'defaultWorkspaceValue', }); - const settingValue = interpreterPathService.get(resource); - expect(settingValue).to.equal('workspaceFolderValue'); }); - test(`Getting setting value returns workspace value if workspace folder value is 'undefined'`, async () => { + test(`Getting setting value returns workspace folder value if it's defined`, async () => { interpreterPathService.inspect = () => ({ globalValue: 'default/path/to/interpreter', - workspaceFolderValue: undefined, - workspaceValue: 'workspaceValue' + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue', }); const settingValue = interpreterPathService.get(resource); - expect(settingValue).to.equal('workspaceValue'); + 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' + workspaceValue: 'workspaceValue', }); const settingValue = interpreterPathService.get(resource); expect(settingValue).to.equal('workspaceValue'); @@ -589,7 +438,7 @@ suite('Interpreter Path Service', async () => { interpreterPathService.inspect = () => ({ globalValue: 'default/path/to/interpreter', workspaceFolderValue: undefined, - workspaceValue: undefined + workspaceValue: undefined, }); const settingValue = interpreterPathService.get(resource); expect(settingValue).to.equal('default/path/to/interpreter'); @@ -599,10 +448,11 @@ suite('Interpreter Path Service', async () => { interpreterPathService.inspect = () => ({ globalValue: undefined, workspaceFolderValue: undefined, - workspaceValue: undefined + workspaceValue: undefined, }); const settingValue = interpreterPathService.get(resource); - expect(settingValue).to.equal('python'); + + expect(settingValue).to.equal(getCIPythonPath()); }); test('If defaultInterpreterPathSetting is changed, an event is fired', async () => { diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 09a84fe3976a..0cdb6f270c54 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -1,12 +1,9 @@ -// tslint:disable:max-func-body-length - -import { expect, should as chai_should, use as chai_use } from 'chai'; +import { expect, should as chaiShould, use as chaiUse } 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 { Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../client/activation/types'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; @@ -14,9 +11,8 @@ import { ApplicationShell } from '../../client/common/application/applicationShe 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 { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; 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 { @@ -25,34 +21,14 @@ import { IApplicationShell, IClipboard, ICommandManager, - ICustomEditorService, IDebugService, IDocumentManager, - ILiveShareApi, - IWorkspaceService + IJupyterExtensionDependencyManager, + 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'; @@ -60,10 +36,6 @@ 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'; @@ -71,11 +43,12 @@ 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 { 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 { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; @@ -92,66 +65,55 @@ import { ITerminalHelper, ITerminalService, ITerminalServiceFactory, - TerminalActivationProviders + 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 + 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 + IComponentAdapter, + IActivatedEnvironmentLaunch, } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; -import { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info'; +import { JupyterExtensionDependencyManager } from '../../client/jupyter/jupyterExtensionDependencyManager'; +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 { PYTHON_PATH } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { closeActiveWindows, initializeTest } from './../initialize'; +import { closeActiveWindows, initializeTest } from '../initialize'; +import { createTypeMoq } from '../mocks/helper'; -chai_use(chaiAsPromised); +chaiUse(chaiAsPromised.default); -const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py'); - -const info: PythonInterpreter = { +const info: PythonEnvironment = { architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: '', - type: InterpreterType.Unknown, + envType: EnvironmentType.Unknown, version: new SemVer('0.0.0-alpha'), sysPrefix: '', - sysVersion: '' + sysVersion: '', }; suite('Module Installer', () => { @@ -159,16 +121,15 @@ suite('Module Installer', () => { let ioc: UnitTestIocContainer; let mockTerminalService: TypeMoq.IMock<ITerminalService>; let condaService: TypeMoq.IMock<ICondaService>; + let condaLocatorService: TypeMoq.IMock<IComponentAdapter>; 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(); + chaiShould(); + await initializeDI(); await initializeTest(); - await resetSettings(); }); suiteTeardown(async () => { await closeActiveWindows(); @@ -178,34 +139,35 @@ suite('Module Installer', () => { await closeActiveWindows(); }); - function initializeDI() { + async 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()); + mockTerminalService = createTypeMoq<ITerminalService>(); + mockTerminalFactory = createTypeMoq<ITerminalServiceFactory>(); // 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)) + .callback((passedInResource) => expect(passedInResource).to.be.deep.equal({ resource })) .returns(() => mockTerminalService.object); ioc.serviceManager.addSingletonInstance<ITerminalServiceFactory>( ITerminalServiceFactory, - mockTerminalFactory.object + mockTerminalFactory.object, + ); + const activatedEnvironmentLaunch = createTypeMoq<IActivatedEnvironmentLaunch>(); + activatedEnvironmentLaunch + .setup((t) => t.selectIfLaunchedViaActivatedEnv()) + .returns(() => Promise.resolve(undefined)); + ioc.serviceManager.addSingletonInstance<IActivatedEnvironmentLaunch>( + IActivatedEnvironmentLaunch, + activatedEnvironmentLaunch.object, ); - ioc.serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller); ioc.serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, CondaInstaller); ioc.serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipEnvInstaller); @@ -221,10 +183,11 @@ suite('Module Installer', () => { ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance<boolean>(IsWindows, false); - ioc.registerMockInterpreterTypes(); - condaService = TypeMoq.Mock.ofType<ICondaService>(); + await ioc.registerMockInterpreterTypes(); + condaService = createTypeMoq<ICondaService>(); + condaLocatorService = createTypeMoq<IComponentAdapter>(); ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, condaService.object); - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService = createTypeMoq<IInterpreterService>(); ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, interpreterService.object); ioc.serviceManager.addSingleton<IActiveResourceService>(IActiveResourceService, ActiveResourceService); @@ -237,141 +200,73 @@ suite('Module Installer', () => { ioc.serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager); ioc.serviceManager.addSingleton<IDebugService>(IDebugService, DebugService); ioc.serviceManager.addSingleton<IApplicationEnvironment>(IApplicationEnvironment, ApplicationEnvironment); + ioc.serviceManager.addSingleton<IJupyterExtensionDependencyManager>( + IJupyterExtensionDependencyManager, + JupyterExtensionDependencyManager, + ); 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 + 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 + TerminalActivationProviders.bashCShellFish, ); ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( ITerminalActivationCommandProvider, CommandPromptAndPowerShell, - TerminalActivationProviders.commandPromptAndPowerShell + TerminalActivationProviders.commandPromptAndPowerShell, + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, ); ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, - TerminalActivationProviders.pyenv + TerminalActivationProviders.pyenv, ); ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( ITerminalActivationCommandProvider, CondaActivationCommandProvider, - TerminalActivationProviders.conda + TerminalActivationProviders.conda, ); ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, - TerminalActivationProviders.pipenv - ); - ioc.serviceManager.addSingleton<IFeatureDeprecationManager>( - IFeatureDeprecationManager, - FeatureDeprecationManager + TerminalActivationProviders.pipenv, ); - 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 + ReloadVSCodeCommandHandler, ); 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 + ReportIssueCommandHandler, ); } - 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 + new MockModuleInstaller('mock', true), ); ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); - - const processService = (await ioc.serviceContainer - .get<IProcessServiceFactory>(IProcessServiceFactory) - .create()) as MockProcessService; + const factory = ioc.serviceManager.get<IProcessServiceFactory>(IProcessServiceFactory); + const processService = (await factory.create()) as MockProcessService; processService.onExec((file, args, _options, callback) => { if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { callback({ stdout: '' }); @@ -399,35 +294,7 @@ suite('Module Installer', () => { 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, - type: InterpreterType.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 + new MockModuleInstaller('mock', true), ); ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); @@ -450,19 +317,25 @@ suite('Module Installer', () => { 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 serviceContainer = createTypeMoq<IServiceContainer>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + const configService = createTypeMoq<IConfigurationService>(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + const settings = createTypeMoq<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); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IComponentAdapter))) + .returns(() => condaLocatorService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IComponentAdapter))) + .returns(() => condaLocatorService.object); condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); - condaService + condaLocatorService .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(true)); @@ -470,19 +343,22 @@ suite('Module Installer', () => { 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 serviceContainer = createTypeMoq<IServiceContainer>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + const configService = createTypeMoq<IConfigurationService>(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + const settings = createTypeMoq<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); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IComponentAdapter))) + .returns(() => condaLocatorService.object); condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); - condaService + condaLocatorService .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(false)); @@ -492,26 +368,10 @@ suite('Module Installer', () => { 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, type: InterpreterType.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: PythonInterpreter = { + const interpreter: PythonEnvironment = { ...info, - type: InterpreterType.Unknown, - path: PYTHON_PATH + envType: EnvironmentType.Unknown, + path: PYTHON_PATH, }; interpreterService .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) @@ -529,39 +389,24 @@ suite('Module Installer', () => { .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); + return Promise.resolve(); }); interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - // tslint:disable-next-line:no-any - .returns(() => Promise.resolve({ type: InterpreterType.Unknown } as any)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-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.' + `-m 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, type: InterpreterType.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); @@ -574,31 +419,19 @@ suite('Module Installer', () => { .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); + return Promise.resolve(); }); await pipInstaller.installModule(moduleName, resource); mockTerminalFactory.verifyAll(); expect(argsSent.join(' ')).equal( - `${isolated} pip install -U ${moduleName}`, - 'Invalid command sent to terminal for installation.' + `-m 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', type: InterpreterType.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')!; @@ -612,7 +445,7 @@ suite('Module Installer', () => { .returns((cmd: string, args: string[]) => { argsSent = args; command = cmd; - return Promise.resolve(void 0); + return Promise.resolve(); }); await pipInstaller.installModule(moduleName, resource); @@ -621,7 +454,7 @@ suite('Module Installer', () => { 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.' + '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 deleted file mode 100644 index 30222665f0ea..000000000000 --- a/src/test/common/net/fileDownloader.unit.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -// 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 { - public get readableLength() { - return 1024 * 10; - } - private readCounter = 0; - constructor( - private readonly totalKb: number, - private readonly delayMs: number, - private readonly kbPerIteration: number - ) { - super(); - } - 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 deleted file mode 100644 index 33f30c4a1176..000000000000 --- a/src/test/common/net/httpClient.unit.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -// 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 deleted file mode 100644 index 2afb1b94b680..000000000000 --- a/src/test/common/nuget/azureBobStoreRepository.functional.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index f29e12e42dcc..000000000000 --- a/src/test/common/nuget/azureBobStoreRepository.unit.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -// 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 deleted file mode 100644 index 1370e824b2da..000000000000 --- a/src/test/common/nuget/nugetRepository.unit.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index 3437db24178d..000000000000 --- a/src/test/common/nuget/nugetService.unit.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -// 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/persistentState.unit.test.ts b/src/test/common/persistentState.unit.test.ts new file mode 100644 index 000000000000..a77ee571559e --- /dev/null +++ b/src/test/common/persistentState.unit.test.ts @@ -0,0 +1,186 @@ +// 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 * as sinon from 'sinon'; +import { Memento } from 'vscode'; +import { ICommandManager } from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { + GLOBAL_PERSISTENT_KEYS_DEPRECATED, + KeysStorage, + PersistentStateFactory, + WORKSPACE_PERSISTENT_KEYS_DEPRECATED, +} from '../../client/common/persistentState'; +import { IDisposable } from '../../client/common/types'; +import { sleep } from '../core'; +import { MockMemento } from '../mocks/mementos'; +import * as apiInt from '../../client/envExt/api.internal'; + +suite('Persistent State', () => { + let cmdManager: TypeMoq.IMock<ICommandManager>; + let persistentStateFactory: PersistentStateFactory; + let workspaceMemento: Memento; + let globalMemento: Memento; + let useEnvExtensionStub: sinon.SinonStub; + setup(() => { + cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); + workspaceMemento = new MockMemento(); + globalMemento = new MockMemento(); + persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento, cmdManager.object); + + useEnvExtensionStub = sinon.stub(apiInt, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + }); + teardown(() => { + sinon.restore(); + }); + + test('Global states created are restored on invoking clean storage command', async () => { + let clearStorageCommand: (() => Promise<void>) | undefined; + cmdManager + .setup((c) => c.registerCommand(Commands.ClearStorage, TypeMoq.It.isAny())) + .callback((_, c) => { + clearStorageCommand = c; + }) + .returns(() => TypeMoq.Mock.ofType<IDisposable>().object); + + // Register command to clean storage + await persistentStateFactory.activate(); + + expect(clearStorageCommand).to.not.equal(undefined, 'Callback not registered'); + + const globalKey1State = persistentStateFactory.createGlobalPersistentState('key1', 'defaultKey1Value'); + await globalKey1State.updateValue('key1Value'); + const globalKey2State = persistentStateFactory.createGlobalPersistentState<string | undefined>( + 'key2', + undefined, + ); + await globalKey2State.updateValue('key2Value'); + + // Verify states are updated correctly + expect(globalKey1State.value).to.equal('key1Value'); + expect(globalKey2State.value).to.equal('key2Value'); + cmdManager + .setup((c) => c.executeCommand('workbench.action.reloadWindow')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await clearStorageCommand!(); // Invoke command + + // Verify states are now reset to their default value. + expect(globalKey1State.value).to.equal('defaultKey1Value'); + expect(globalKey2State.value).to.equal(undefined); + cmdManager.verifyAll(); + }); + + test('Workspace states created are restored on invoking clean storage command', async () => { + let clearStorageCommand: (() => Promise<void>) | undefined; + cmdManager + .setup((c) => c.registerCommand(Commands.ClearStorage, TypeMoq.It.isAny())) + .callback((_, c) => { + clearStorageCommand = c; + }) + .returns(() => TypeMoq.Mock.ofType<IDisposable>().object); + + // Register command to clean storage + await persistentStateFactory.activate(); + + expect(clearStorageCommand).to.not.equal(undefined, 'Callback not registered'); + + const workspaceKey1State = persistentStateFactory.createWorkspacePersistentState('key1'); + await workspaceKey1State.updateValue('key1Value'); + const workspaceKey2State = persistentStateFactory.createWorkspacePersistentState('key2', 'defaultKey2Value'); + await workspaceKey2State.updateValue('key2Value'); + + // Verify states are updated correctly + expect(workspaceKey1State.value).to.equal('key1Value'); + expect(workspaceKey2State.value).to.equal('key2Value'); + cmdManager + .setup((c) => c.executeCommand('workbench.action.reloadWindow')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await clearStorageCommand!(); // Invoke command + + // Verify states are now reset to their default value. + expect(workspaceKey1State.value).to.equal(undefined); + expect(workspaceKey2State.value).to.equal('defaultKey2Value'); + cmdManager.verifyAll(); + }); + + test('Ensure internal global storage extension uses to track other storages does not contain duplicate entries', async () => { + persistentStateFactory.createGlobalPersistentState('key1'); + await sleep(1); + persistentStateFactory.createGlobalPersistentState('key2', ['defaultValue1']); // Default value type is an array + await sleep(1); + persistentStateFactory.createGlobalPersistentState('key2', ['defaultValue1']); + await sleep(1); + persistentStateFactory.createGlobalPersistentState('key1'); + await sleep(1); + const { value } = persistentStateFactory._globalKeysStorage; + assert.deepEqual( + value.sort((k1, k2) => k1.key.localeCompare(k2.key)), + [ + { key: 'key1', defaultValue: undefined }, + { key: 'key2', defaultValue: ['defaultValue1'] }, + ].sort((k1, k2) => k1.key.localeCompare(k2.key)), + ); + }); + + test('Ensure internal workspace storage extension uses to track other storages does not contain duplicate entries', async () => { + persistentStateFactory.createWorkspacePersistentState('key2', 'defaultValue1'); // Default value type is a string + await sleep(1); + persistentStateFactory.createWorkspacePersistentState('key1'); + await sleep(1); + persistentStateFactory.createWorkspacePersistentState('key2', 'defaultValue1'); + await sleep(1); + persistentStateFactory.createWorkspacePersistentState('key1'); + await sleep(1); + const { value } = persistentStateFactory._workspaceKeysStorage; + assert.deepEqual( + value.sort((k1, k2) => k1.key.localeCompare(k2.key)), + [ + { key: 'key1', defaultValue: undefined }, + { key: 'key2', defaultValue: 'defaultValue1' }, + ].sort((k1, k2) => k1.key.localeCompare(k2.key)), + ); + }); + + test('Ensure deprecated global storage extension used to track other storages with is reset', async () => { + const global = persistentStateFactory.createGlobalPersistentState<KeysStorage[]>( + GLOBAL_PERSISTENT_KEYS_DEPRECATED, + ); + await global.updateValue([ + { key: 'oldKey', defaultValue: [] }, + { key: 'oldKey2', defaultValue: [{}] }, + { key: 'oldKey3', defaultValue: ['1', '2', '3'] }, + ]); + expect(global.value.length).to.equal(3); + + await persistentStateFactory.activate(); + await sleep(1); + + expect(global.value.length).to.equal(0); + }); + + test('Ensure deprecated global storage extension used to track other storages with is reset', async () => { + const workspace = persistentStateFactory.createWorkspacePersistentState<KeysStorage[]>( + WORKSPACE_PERSISTENT_KEYS_DEPRECATED, + ); + await workspace.updateValue([ + { key: 'oldKey', defaultValue: [] }, + { key: 'oldKey2', defaultValue: [{}] }, + { key: 'oldKey3', defaultValue: ['1', '2', '3'] }, + ]); + expect(workspace.value.length).to.equal(3); + + await persistentStateFactory.activate(); + await sleep(1); + + expect(workspace.value.length).to.equal(0); + }); +}); diff --git a/src/test/common/platform/errors.unit.test.ts b/src/test/common/platform/errors.unit.test.ts index d565bc817218..85a822978ef2 100644 --- a/src/test/common/platform/errors.unit.test.ts +++ b/src/test/common/platform/errors.unit.test.ts @@ -1,8 +1,6 @@ // 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 { @@ -10,7 +8,7 @@ import { isFileIsDirError, isFileNotFoundError, isNoPermissionsError, - isNotDirError + isNotDirError, } from '../../../client/common/platform/errors'; import { SystemError } from './utils'; @@ -23,7 +21,7 @@ suite('FileSystem - errors', () => { [vscode.FileSystemError.FileExists(filename), false], [new SystemError('ENOENT', 'stat', '<msg>'), true], [new SystemError('EEXIST', '???', '<msg>'), false], - [new Error(filename), undefined] + [new Error(filename), undefined], ]; tests.map(([err, expected]) => { test(`${err} -> ${expected}`, () => { @@ -40,7 +38,7 @@ suite('FileSystem - errors', () => { [vscode.FileSystemError.FileNotFound(filename), false], [new SystemError('EEXIST', '???', '<msg>'), true], [new SystemError('ENOENT', 'stat', '<msg>'), false], - [new Error(filename), undefined] + [new Error(filename), undefined], ]; tests.map(([err, expected]) => { test(`${err} -> ${expected}`, () => { @@ -57,7 +55,7 @@ suite('FileSystem - errors', () => { [vscode.FileSystemError.FileNotFound(filename), false], [new SystemError('EISDIR', '???', '<msg>'), true], [new SystemError('ENOENT', 'stat', '<msg>'), false], - [new Error(filename), undefined] + [new Error(filename), undefined], ]; tests.map(([err, expected]) => { test(`${err} -> ${expected}`, () => { @@ -74,7 +72,7 @@ suite('FileSystem - errors', () => { [vscode.FileSystemError.FileNotFound(filename), false], [new SystemError('ENOTDIR', '???', '<msg>'), true], [new SystemError('ENOENT', 'stat', '<msg>'), false], - [new Error(filename), undefined] + [new Error(filename), undefined], ]; tests.map(([err, expected]) => { test(`${err} -> ${expected}`, () => { @@ -91,7 +89,7 @@ suite('FileSystem - errors', () => { [vscode.FileSystemError.FileNotFound(filename), false], [new SystemError('EACCES', '???', '<msg>'), true], [new SystemError('ENOENT', 'stat', '<msg>'), false], - [new Error(filename), undefined] + [new Error(filename), undefined], ]; tests.map(([err, expected]) => { test(`${err} -> ${expected}`, () => { diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts index 55e96b7aaa5d..be9a369935f3 100644 --- a/src/test/common/platform/filesystem.functional.test.ts +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -1,12 +1,9 @@ // 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 * as fs 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'; @@ -18,10 +15,9 @@ import { FSFixture, SUPPORTS_SOCKETS, SUPPORTS_SYMLINKS, - WINDOWS + WINDOWS, } from './utils'; -// tslint:disable:no-require-imports no-var-requires const assertArrays = require('chai-arrays'); use(require('chai-as-promised')); use(assertArrays); @@ -43,7 +39,6 @@ suite('FileSystem - raw', () => { 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', '...'); @@ -80,7 +75,6 @@ suite('FileSystem - raw', () => { 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(); } }); @@ -142,7 +136,7 @@ suite('FileSystem - raw', () => { await fileSystem.appendText(filename, dataToAppend); - const actual = await fs.readFile(filename, 'utf8'); + const actual = await fs.readFile(filename, { encoding: 'utf8' }); expect(actual).to.be.equal(expected); }); @@ -153,14 +147,14 @@ suite('FileSystem - raw', () => { await fileSystem.appendText(filename, dataToAppend); - const actual = await fs.readFile(filename, 'utf8'); + const actual = await fs.readFile(filename, { encoding: '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'); + const actual = await fs.readFile(DOES_NOT_EXIST, { encoding: 'utf8' }); expect(actual).to.be.equal('spam'); }); @@ -191,7 +185,6 @@ suite('FileSystem - raw', () => { 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', '...'); @@ -239,12 +232,11 @@ suite('FileSystem - raw', () => { 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(); }); @@ -267,12 +259,11 @@ suite('FileSystem - raw', () => { 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(); }); @@ -410,7 +401,7 @@ suite('FileSystem - utils', () => { 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') + await fix.createDirectory('x/y/z/spam.data'), ]; // non-matches await fix.createFile('x/spam.py'); @@ -459,7 +450,6 @@ suite('FileSystem - utils', () => { 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', '...'); @@ -476,7 +466,6 @@ suite('FileSystem - utils', () => { 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'); @@ -507,8 +496,8 @@ suite('FileSystem', () => { }); suite('path-related', () => { - const paths = FileSystemPaths.withDefaults(); - const pathUtils = FileSystemPathUtils.withDefaults(paths); + const paths = fs.FileSystemPaths.withDefaults(); + const pathUtils = fs.FileSystemPathUtils.withDefaults(paths); suite('directorySeparatorChar', () => { // tested fully in the FileSystemPaths tests. @@ -546,7 +535,7 @@ suite('FileSystem', () => { await fileSystem.appendFile(filename, dataToAppend); - const actual = await fs.readFile(filename, 'utf8'); + const actual = await fs.readFile(filename, { encoding: 'utf8' }); expect(actual).to.be.equal(expected); }); }); @@ -555,7 +544,6 @@ suite('FileSystem', () => { 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(); } }); @@ -588,7 +576,7 @@ suite('FileSystem', () => { 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', '...'); @@ -605,7 +593,7 @@ suite('FileSystem', () => { 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'); @@ -689,7 +677,7 @@ suite('FileSystem', () => { 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') + await fix.createDirectory('x/y/z/spam.data'), ]; // non-matches await fix.createFile('x/spam.py'); @@ -709,7 +697,7 @@ suite('FileSystem', () => { 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') + await fix.createFile('x/y/z/.net.py'), ]; // non-matches await fix.createFile('x/spam.py'); @@ -758,7 +746,6 @@ suite('FileSystem', () => { 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', '...'); @@ -775,7 +762,6 @@ suite('FileSystem', () => { 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'); diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts index 091d1b7a0e63..a1afab02d1fe 100644 --- a/src/test/common/platform/filesystem.test.ts +++ b/src/test/common/platform/filesystem.test.ts @@ -1,11 +1,8 @@ // 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 fsextra from '../../../client/common/platform/fs-paths'; 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'; @@ -17,7 +14,7 @@ import { FSFixture, SUPPORTS_SOCKETS, SUPPORTS_SYMLINKS, - WINDOWS + WINDOWS, } from './utils'; // Note: all functional tests that do not trigger the VS Code "fs" API @@ -37,6 +34,11 @@ suite('FileSystem - raw', () => { }); suite('stat', () => { + setup(function () { + // https://github.com/microsoft/vscode-python/issues/10294 + + 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); @@ -59,7 +61,6 @@ suite('FileSystem - raw', () => { 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', '...'); @@ -74,7 +75,6 @@ suite('FileSystem - raw', () => { 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'); @@ -124,7 +124,6 @@ suite('FileSystem - raw', () => { test('rename symlink', async function () { if (!SUPPORTS_SYMLINKS) { - // tslint:disable-next-line:no-invalid-this this.skip(); } const filename = await fix.createFile('spam.py'); @@ -172,7 +171,6 @@ suite('FileSystem - raw', () => { test('move symlink', async function () { if (!SUPPORTS_SYMLINKS) { - // tslint:disable-next-line:no-invalid-this this.skip(); } const filename = await fix.createFile('spam.py'); @@ -471,7 +469,7 @@ suite('FileSystem - raw', () => { 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'); @@ -485,21 +483,21 @@ suite('FileSystem - raw', () => { [script, FileType.File], [file3, FileType.File], [file2, FileType.File], - [subdir, FileType.Directory] + [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 + 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 + await fix.resolve('x/y/z/w/data'), // source ); expected.push([symlink4, FileType.SymbolicLink | FileType.Directory]); @@ -521,12 +519,12 @@ suite('FileSystem - raw', () => { // a symlink to a socket const symlink3 = await fix.createSymlink( 'x/y/z/ipc.sck', - sock // source + 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 | FileType.File], //[symlink3, FileType.SymbolicLink] ); } @@ -542,20 +540,20 @@ suite('FileSystem - raw', () => { // VS Code ignores broken symlinks currently... await fix.createSymlink( 'x/y/z/broken', - DOES_NOT_EXIST // source + 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) + 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 + await fix.createFile('x/__init__.py', ''), // source ); } @@ -677,7 +675,6 @@ suite('FileSystem - utils', () => { 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', '...'); @@ -702,7 +699,6 @@ suite('FileSystem - utils', () => { 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'); @@ -740,7 +736,6 @@ suite('FileSystem - utils', () => { 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', '...'); @@ -754,7 +749,6 @@ suite('FileSystem - utils', () => { 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'); @@ -766,7 +760,6 @@ suite('FileSystem - utils', () => { 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'); @@ -803,7 +796,6 @@ suite('FileSystem - utils', () => { 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'); @@ -817,7 +809,6 @@ suite('FileSystem - utils', () => { 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'); @@ -829,7 +820,6 @@ suite('FileSystem - utils', () => { 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'); @@ -859,7 +849,7 @@ suite('FileSystem - utils', () => { expect(entries.sort()).to.deep.equal([ [file, FileType.File], - [subdir, FileType.Directory] + [subdir, FileType.Directory], ]); }); }); @@ -885,7 +875,6 @@ suite('FileSystem - utils', () => { suite('non-Windows', () => { suiteSetup(function () { if (WINDOWS) { - // tslint:disable-next-line:no-invalid-this this.skip(); } }); @@ -932,10 +921,12 @@ suite('FileSystem', () => { suite('raw', () => { suite('stat', () => { - test('gets the info for an existing file', async function () { + setup(function () { // https://github.com/microsoft/vscode-python/issues/10294 - // tslint:disable-next-line: no-invalid-this - return this.skip(); + + 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); @@ -957,10 +948,9 @@ suite('FileSystem', () => { 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', '...'); @@ -975,7 +965,6 @@ suite('FileSystem', () => { 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'); @@ -1030,7 +1019,7 @@ suite('FileSystem', () => { expect(entries.sort()).to.deep.equal([ [file, FileType.File], - [subdir, FileType.Directory] + [subdir, FileType.Directory], ]); }); }); @@ -1120,7 +1109,6 @@ suite('FileSystem', () => { 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', '...'); @@ -1134,7 +1122,6 @@ suite('FileSystem', () => { 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'); @@ -1164,7 +1151,6 @@ suite('FileSystem', () => { 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'); @@ -1178,7 +1164,6 @@ suite('FileSystem', () => { 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'); @@ -1195,7 +1180,7 @@ suite('FileSystem', () => { 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 + await fix.createDirectory('x/y/z/scripts/v'), // subdir2 ]; if (SUPPORTS_SYMLINKS) { // a symlink to a directory (source is outside listed dir) @@ -1236,7 +1221,7 @@ suite('FileSystem', () => { 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 + await fix.createFile('x/y/z/scripts/data.json'), // file3 ]; if (SUPPORTS_SYMLINKS) { const symlinkFileSource = await fix.createFile('x/info.py'); @@ -1270,7 +1255,6 @@ suite('FileSystem', () => { suite('non-Windows', () => { suiteSetup(function () { if (WINDOWS) { - // tslint:disable-next-line:no-invalid-this this.skip(); } }); diff --git a/src/test/common/platform/filesystem.unit.test.ts b/src/test/common/platform/filesystem.unit.test.ts index 00e0ca55c9e3..f012cb9fb27e 100644 --- a/src/test/common/platform/filesystem.unit.test.ts +++ b/src/test/common/platform/filesystem.unit.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import * as fs from 'fs'; -import * as fsextra from 'fs-extra'; +import * as fsextra from '../../../client/common/platform/fs-paths'; import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import { FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; @@ -16,17 +16,14 @@ import { IRawFileSystem, ITempFileSystem, ReadStream, - WriteStream + 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; } @@ -58,6 +55,7 @@ interface IRawFS extends IPaths { writeFile(uri: vscode.Uri, content: Uint8Array): Thenable<void>; // "fs-extra" + pathExists(filename: string): Promise<boolean>; lstat(filename: string): Promise<fs.Stats>; chmod(filePath: string, mode: string | number): Promise<void>; appendFile(filename: string, data: {}): Promise<void>; @@ -79,7 +77,7 @@ suite('Raw FileSystem', () => { // Since it's a mock we can just use it for all 3 values. raw.object, raw.object, - raw.object + raw.object, ); }); function verifyAll() { @@ -92,7 +90,6 @@ suite('Raw FileSystem', () => { 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)); @@ -166,7 +163,7 @@ suite('Raw FileSystem', () => { { kind: 'file', filetype: FileType.File }, { kind: 'dir', filetype: FileType.Directory }, { kind: 'symlink', filetype: FileType.SymbolicLink }, - { kind: 'unknown', filetype: FileType.Unknown } + { kind: 'unknown', filetype: FileType.Unknown }, ].forEach((testData) => { test(`wraps the low-level function (filetype: ${testData.kind}`, async () => { const filename = 'x/y/z/spam.py'; @@ -174,8 +171,7 @@ suite('Raw FileSystem', () => { type: testData.filetype, size: 10, ctime: 101, - mtime: 102 - //tslint:disable-next-line:no-any + mtime: 102, } as any; const old = createMockLegacyStat(); setupStatFileType(old, testData.filetype); @@ -317,7 +313,7 @@ suite('Raw FileSystem', () => { .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) + 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()); @@ -499,7 +495,7 @@ suite('Raw FileSystem', () => { suite('rmFile', () => { const opts = { recursive: false, - useTrash: false + useTrash: false, }; test('wraps the low-level function', async () => { @@ -548,7 +544,7 @@ suite('Raw FileSystem', () => { suite('rmdir', () => { const opts = { recursive: true, - useTrash: false + useTrash: false, }; test('directory is empty', async () => { @@ -578,7 +574,7 @@ suite('Raw FileSystem', () => { ['dev1', FileType.Unknown], ['w', FileType.Directory], ['spam.py', FileType.File], - ['other', FileType.SymbolicLink | FileType.File] + ['other', FileType.SymbolicLink | FileType.File], ]; raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // The dir is not empty. .returns(() => Promise.resolve(entries)); @@ -605,7 +601,7 @@ suite('Raw FileSystem', () => { suite('rmtree', () => { const opts = { recursive: true, - useTrash: false + useTrash: false, }; test('wraps the low-level function', async () => { @@ -640,7 +636,7 @@ suite('Raw FileSystem', () => { ['dev1', FileType.Unknown], ['w', FileType.Directory], ['spam.py', FileType.File], - ['other', FileType.SymbolicLink | FileType.File] + ['other', FileType.SymbolicLink | FileType.File], ]; const expected = actual.map(([basename, filetype]) => { const filename = `x/y/z/spam/${basename}`; @@ -687,8 +683,7 @@ suite('Raw FileSystem', () => { type: FileType.Unknown, size: 10, ctime: 101, - mtime: 102 - //tslint:disable-next-line:no-any + mtime: 102, } as any; const lstat = createMockLegacyStat(); setupStatFileType(lstat, FileType.Unknown); @@ -704,7 +699,7 @@ suite('Raw FileSystem', () => { [ { kind: 'file', filetype: FileType.File }, - { kind: 'dir', filetype: FileType.Directory } + { kind: 'dir', filetype: FileType.Directory }, ].forEach((testData) => { test(`wraps the low-level function (filetype: ${testData.kind})`, async () => { const filename = 'x/y/z/spam.py'; @@ -712,8 +707,7 @@ suite('Raw FileSystem', () => { type: testData.filetype, size: 10, ctime: 101, - mtime: 102 - //tslint:disable-next-line:no-any + mtime: 102, } as any; const lstat = createMockLegacyStat(); lstat @@ -734,7 +728,7 @@ suite('Raw FileSystem', () => { [ { kind: 'file', filetype: FileType.File }, { kind: 'dir', filetype: FileType.Directory }, - { kind: 'unknown', filetype: FileType.Unknown } + { 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'; @@ -742,8 +736,7 @@ suite('Raw FileSystem', () => { type: testData.filetype | FileType.SymbolicLink, size: 10, ctime: 101, - mtime: 102 - //tslint:disable-next-line:no-any + mtime: 102, } as any; const lstat = createMockLegacyStat(); lstat @@ -801,7 +794,6 @@ suite('Raw FileSystem', () => { 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); @@ -825,7 +817,6 @@ suite('Raw FileSystem', () => { 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); @@ -868,7 +859,7 @@ suite('FileSystemUtils', () => { deps.object, // paths deps.object, // tempFS (data: string) => deps.object.getHash(data), - (pat: string, options?: { cwd: string }) => deps.object.globFile(pat, options) + (pat: string, options?: { cwd: string }) => deps.object.globFile(pat, options), ); }); function verifyAll() { @@ -881,7 +872,6 @@ suite('FileSystemUtils', () => { 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)); @@ -928,9 +918,8 @@ suite('FileSystemUtils', () => { 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)); + deps.setup((d) => d.pathExists(filename)) // The "file" exists. + .returns(() => Promise.resolve(true)); const exists = await utils.pathExists(filename); @@ -938,11 +927,10 @@ suite('FileSystemUtils', () => { verifyAll(); }); - test('does not exist', async () => { + test('does not exist (without type)', 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); + deps.setup((d) => d.pathExists(filename)) // The "file" exists. + .returns(() => Promise.resolve(false)); const exists = await utils.pathExists(filename); @@ -950,29 +938,6 @@ suite('FileSystemUtils', () => { 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(); @@ -1217,7 +1182,7 @@ suite('FileSystemUtils', () => { ['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] + ['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)); @@ -1233,8 +1198,8 @@ suite('FileSystemUtils', () => { 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)); + deps.setup((d) => d.pathExists(dirname)) // The "file" does not exist. + .returns(() => Promise.resolve(false)); const entries = await utils.listdir(dirname); @@ -1247,9 +1212,7 @@ suite('FileSystemUtils', () => { 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)); + deps.setup((d) => d.pathExists(dirname)).returns(() => Promise.resolve(true)); // The "file" exists. const promise = utils.listdir(dirname); @@ -1262,29 +1225,13 @@ suite('FileSystemUtils', () => { 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)); + deps.setup((d) => d.pathExists(dirname)).returns(() => Promise.resolve(false)); 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', () => { @@ -1297,13 +1244,13 @@ suite('FileSystemUtils', () => { ['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] + ['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' + 'x/y/z/spam/other2', ]; deps.setup((d) => d.listdir(dirname)) // Full results get returned from RawFileSystem.listdir(). .returns(() => Promise.resolve(entries)); @@ -1325,13 +1272,13 @@ suite('FileSystemUtils', () => { ['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] + ['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' + 'x/y/z/spam/other1', ]; deps.setup((d) => d.listdir(filename)) // Full results get returned from RawFileSystem.listdir(). .returns(() => Promise.resolve(entries)); @@ -1369,7 +1316,7 @@ suite('FileSystemUtils', () => { 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)); @@ -1385,7 +1332,7 @@ suite('FileSystemUtils', () => { 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)); @@ -1438,7 +1385,7 @@ suite('FileSystemUtils', () => { 'x/y/z/spam.py', 'x/y/z/spam.pyc', 'x/y/z/spam.so', - 'x/y/z/spam.data' + 'x/y/z/spam.data', ]; deps.setup((d) => d.globFile(pattern, undefined)) // found some .returns(() => Promise.resolve(expected)); @@ -1458,7 +1405,7 @@ suite('FileSystemUtils', () => { 'x/y/z/spam.py', 'x/y/z/spam.pyc', 'x/y/z/spam.so', - 'x/y/z/spam.data' + 'x/y/z/spam.data', ]; deps.setup((d) => d.globFile(pattern, { cwd: cwd })) // found some .returns(() => Promise.resolve(expected)); diff --git a/src/test/common/platform/fs-paths.functional.test.ts b/src/test/common/platform/fs-paths.functional.test.ts index 3e3925a0ea0c..a7e6bfd0559d 100644 --- a/src/test/common/platform/fs-paths.functional.test.ts +++ b/src/test/common/platform/fs-paths.functional.test.ts @@ -3,8 +3,6 @@ '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'; @@ -110,7 +108,7 @@ suite('FileSystem - Paths', () => { // Be explicit here to ensure our assumptions are correct // about the relationship between "sep" and "join()". path.sep === '\\' ? 'y\\z' : 'y/z', - 'spam.py' + 'spam.py', ); expect(result).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 index bfd588efdd49..b34b65d01e53 100644 --- a/src/test/common/platform/fs-paths.unit.test.ts +++ b/src/test/common/platform/fs-paths.unit.test.ts @@ -1,8 +1,6 @@ // 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'; @@ -35,7 +33,7 @@ suite('FileSystem - Path Utils', () => { // It's simpler to just use one mock for all 3 dependencies. deps.object, deps.object, - deps.object + deps.object, ); }); function verifyAll() { @@ -64,7 +62,7 @@ suite('FileSystem - Path Utils', () => { // no upper-case 'c:\\users\\peter smith\\my documents\\test.txt', // some upper-case - 'c:\\USERS\\Peter Smith\\my documents\\test.TXT' + 'c:\\USERS\\Peter Smith\\my documents\\test.TXT', ].forEach((path1) => { test(`True if paths are identical (type: ${item.name}) - ${path1}`, () => { path1 = setNormCase(path1, 2); diff --git a/src/test/common/platform/fs-temp.functional.test.ts b/src/test/common/platform/fs-temp.functional.test.ts index 226f2f734814..67bca3338e76 100644 --- a/src/test/common/platform/fs-temp.functional.test.ts +++ b/src/test/common/platform/fs-temp.functional.test.ts @@ -1,13 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:max-func-body-length chai-vague-errors - -import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import { expect, use } from 'chai'; +import * as fs from '../../../client/common/platform/fs-paths'; import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; import { TemporaryFile } from '../../../client/common/platform/types'; -import { assertDoesNotExist, assertExists, FSFixture, WINDOWS } from './utils'; +import { assertDoesNotExist, assertExists, FSFixture } from './utils'; + +const assertArrays = require('chai-arrays'); +use(require('chai-as-promised')); +use(assertArrays); suite('FileSystem - TemporaryFileSystem', () => { let tmpfs: TemporaryFileSystem; @@ -54,22 +56,6 @@ suite('FileSystem - TemporaryFileSystem', () => { 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'); diff --git a/src/test/common/platform/fs-temp.unit.test.ts b/src/test/common/platform/fs-temp.unit.test.ts index 62d5d8932498..29b4e5f42b12 100644 --- a/src/test/common/platform/fs-temp.unit.test.ts +++ b/src/test/common/platform/fs-temp.unit.test.ts @@ -1,19 +1,20 @@ // 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; + fileSync(config: { + postfix?: string; + mode?: number; + }): { + name: string; + fd: number; + removeCallback(): void; + }; } suite('FileSystem - temp files', () => { @@ -30,7 +31,7 @@ suite('FileSystem - temp files', () => { 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())) + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })) // fail with an arbitrary error .throws(failure); @@ -42,9 +43,9 @@ suite('FileSystem - temp files', () => { 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, () => {})); + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })).callback((_cfg, cb) => + cb(failure, '...', -1, () => {}), + ); const promise = temp.createFile('.tmp'); diff --git a/src/test/common/platform/pathUtils.functional.test.ts b/src/test/common/platform/pathUtils.functional.test.ts index 7a7cc9b88e92..35938f687b3b 100644 --- a/src/test/common/platform/pathUtils.functional.test.ts +++ b/src/test/common/platform/pathUtils.functional.test.ts @@ -3,8 +3,6 @@ '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'; diff --git a/src/test/common/platform/platformService.functional.test.ts b/src/test/common/platform/platformService.functional.test.ts new file mode 100644 index 000000000000..9f16a6ebf386 --- /dev/null +++ b/src/test/common/platform/platformService.functional.test.ts @@ -0,0 +1,114 @@ +// 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.default); + +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 () => { + // eslint-disable-next-line global-require + 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) { + 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'); + + return undefined; + }); + test('getVersion on Linux shoud throw an exception', async function () { + if (osType !== OSType.Linux) { + return this.skip(); + } + const svc = new PlatformService(); + + await expect(svc.getVersion()).to.eventually.be.rejectedWith('Not Supported'); + + return undefined; + }); +}); + +function getOSType(platform: string = process.platform): OSType { + if (/^win/.test(platform)) { + return OSType.Windows; + } + if (/^darwin/.test(platform)) { + return OSType.OSX; + } + if (/^linux/.test(platform)) { + return OSType.Linux; + } + return OSType.Unknown; +} diff --git a/src/test/common/platform/platformService.test.ts b/src/test/common/platform/platformService.test.ts deleted file mode 100644 index 0f803e38095e..000000000000 --- a/src/test/common/platform/platformService.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -// 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/utils.ts b/src/test/common/platform/utils.ts index 89d98e61274f..881e3cd019b9 100644 --- a/src/test/common/platform/utils.ts +++ b/src/test/common/platform/utils.ts @@ -1,15 +1,15 @@ // 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 fsextra from '../../../client/common/platform/fs-paths'; import * as net from 'net'; import * as path from 'path'; import * as tmpMod from 'tmp'; import { CleanupFixture } from '../../fixtures'; +// XXX Move most of this file to src/test/utils/fs.ts and src/test/fixtures.ts. + // Note: all functional tests that trigger the VS Code "fs" API are // found in filesystem.test.ts. @@ -37,7 +37,7 @@ export const SUPPORTS_SOCKETS = (() => { } const tmp = tmpMod.dirSync({ prefix: 'pyvsc-test-', - unsafeCleanup: true // for non-empty dir + unsafeCleanup: true, // for non-empty dir }); const filename = path.join(tmp.name, 'test.sock'); try { @@ -140,7 +140,7 @@ export class FSFixture extends CleanupFixture { 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)); + await new Promise<void>((resolve) => srv!.listen(filename, 0, resolve)); return filename; } @@ -178,7 +178,7 @@ export class FSFixture extends CleanupFixture { const tempDir = tmpMod.dirSync({ prefix: 'pyvsc-fs-tests-', - unsafeCleanup: true + unsafeCleanup: true, }); this.tempDir = tempDir.name; diff --git a/src/test/common/process/decoder.test.ts b/src/test/common/process/decoder.test.ts index 92d43f8aadf4..6123ce2a447c 100644 --- a/src/test/common/process/decoder.test.ts +++ b/src/test/common/process/decoder.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { encode, encodingExists } from 'iconv-lite'; -import { BufferDecoder } from '../../../client/common/process/decoder'; +import { decodeBuffer } from '../../../client/common/process/decoder'; import { initialize } from './../../initialize'; suite('Decoder', () => { @@ -13,23 +13,20 @@ suite('Decoder', () => { test('Test decoding utf8 strings', () => { const value = 'Sample input string Сделать это'; const buffer = encode(value, 'utf8'); - const decoder = new BufferDecoder(); - const decodedValue = decoder.decode([buffer]); + const decodedValue = decodeBuffer([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]); + let decodedValue = decodeBuffer([buffer]); expect(decodedValue).not.equal(value, 'Decoded string is the same'); - decodedValue = decoder.decode([buffer], 'cp866'); + decodedValue = decodeBuffer([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 deleted file mode 100644 index d1d0400c55c3..000000000000 --- a/src/test/common/process/execFactory.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// 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 index d6c0706c57c7..366a7056e89e 100644 --- a/src/test/common/process/logger.unit.test.ts +++ b/src/test/common/process/logger.unit.test.ts @@ -3,119 +3,209 @@ 'use strict'; -import { 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 { PathUtils } from '../../../client/common/platform/pathUtils'; +import { WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; 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'; +import { getOSType, OSType } from '../../../client/common/utils/platform'; +import * as logging from '../../../client/logging'; +import { untildify } from '../../../client/common/helpers'; -// 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); + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let logger: ProcessLogger; + let traceLogStub: sinon.SinonStub; + + suiteSetup(async () => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [({ uri: { fsPath: path.join('path', 'to', 'workspace') } } as unknown) as WorkspaceFolder]); + logger = new ProcessLogger(workspaceService.object); }); setup(() => { - outputResult = ''; - outputChannel - .setup((o) => o.appendLine(TypeMoq.It.isAnyString())) - .returns((s: string) => (outputResult += `${s}\n`)); + traceLogStub = sinon.stub(logging, 'traceLog'); }); teardown(() => { - outputChannel.reset(); + sinon.restore(); }); 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)); + sinon.assert.calledWithExactly(traceLogStub, `> test --foo --bar`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); }); 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'); + sinon.assert.calledWithExactly(traceLogStub, `> test --foo --bar "import test"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${path.join('debug', 'path')}`); }); 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); + + sinon.assert.calledWithExactly(traceLogStub, `> test --foo --bar "import test"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${path.join('debug', 'path')}`); + }); + + test('Logger converts single quotes around arguments to double quotes if they contain spaces', async () => { + const options = { cwd: path.join('debug', 'path') }; 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'); + sinon.assert.calledWithExactly(traceLogStub, `> test --foo --bar "import test"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${path.join('debug', 'path')}`); + }); + + test('Logger removes single quotes around arguments if they do not contain spaces', async () => { + const options = { cwd: path.join('debug', 'path') }; + logger.logProcess('test', ['--foo', '--bar', "'importtest'"], options); + + sinon.assert.calledWithExactly(traceLogStub, `> test --foo --bar importtest`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${path.join('debug', 'path')}`); }); 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'); + sinon.assert.calledWithExactly(traceLogStub, `> test --foo --bar`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${path.join('~', 'debug', 'path')}`); }); - test('Logger replaces the path/to/home with ~ in the command path', async () => { + test('Logger replaces the path/to/home with ~ in the command path where the home path IS at the beginning of the 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'); + sinon.assert.calledWithExactly(traceLogStub, `> ${path.join('~', 'test')} --foo --bar`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); + }); + + test('Logger replaces the path/to/home with ~ in the command path where the home path IS at the beginning of the path but another arg contains other ref to home folder', async () => { + const options = { cwd: path.join('debug', 'path') }; + logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', path.join(untildify('~'), 'boo')], options); + + sinon.assert.calledWithExactly(traceLogStub, `> ${path.join('~', 'test')} --foo ${path.join('~', 'boo')}`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); + }); + + test('Logger replaces the path/to/home with ~ in the command path where the home path IS at the beginning of the path between doble quotes', async () => { + const options = { cwd: path.join('debug', 'path') }; + logger.logProcess(`"${path.join(untildify('~'), 'test')}" "--foo" "--bar"`, undefined, options); + + sinon.assert.calledWithExactly(traceLogStub, `> "${path.join('~', 'test')}" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); + }); + + test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path', async () => { + const options = { cwd: path.join('debug', 'path') }; + const untildifyStr = untildify('~'); + + let p1 = path.join('net', untildifyStr, 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(p1, ['--foo', '--bar'], options); + + const path1 = path.join('.', 'net', '~', 'test'); + sinon.assert.calledWithExactly(traceLogStub, `> ${path1} --foo --bar`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); + }); + + test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path but another arg contains other ref to home folder', async () => { + const options = { cwd: path.join('debug', 'path') }; + let p1 = path.join('net', untildify('~'), 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(p1, ['--foo', path.join(untildify('~'), 'boo')], options); + + sinon.assert.calledWithExactly( + traceLogStub, + `> ${path.join('.', 'net', '~', 'test')} --foo ${path.join('~', 'boo')}`, + ); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); + }); + + test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path between doble quotes', async () => { + const options = { cwd: path.join('debug', 'path') }; + let p1 = path.join('net', untildify('~'), 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(`"${p1}" "--foo" "--bar"`, undefined, options); + + sinon.assert.calledWithExactly(traceLogStub, `> "${path.join('.', 'net', '~', 'test')}" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); + }); + + test('Logger replaces the path/to/home with ~ if shell command is provided', async () => { + const options = { cwd: path.join('debug', 'path') }; + logger.logProcess(`"${path.join(untildify('~'), 'test')}" "--foo" "--bar"`, undefined, options); + + sinon.assert.calledWithExactly(traceLogStub, `> "${path.join('~', 'test')}" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); + }); + + test('Logger replaces the path to workspace with . if exactly one workspace folder is opened', async () => { + const options = { cwd: path.join('path', 'to', 'workspace', 'debug', 'path') }; + logger.logProcess(`"${path.join('path', 'to', 'workspace', 'test')}" "--foo" "--bar"`, undefined, options); + + sinon.assert.calledWithExactly(traceLogStub, `> ".${path.sep}test" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: .${path.sep + path.join('debug', 'path')}`); + }); + + test('On Windows, logger replaces both backwards and forward slash version of path to workspace with . if exactly one workspace folder is opened', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + let options = { cwd: path.join('path/to/workspace', 'debug', 'path') }; + + logger.logProcess(`"${path.join('path', 'to', 'workspace', 'test')}" "--foo" "--bar"`, undefined, options); + + sinon.assert.calledWithExactly(traceLogStub, `> ".${path.sep}test" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: .${path.sep + path.join('debug', 'path')}`); + traceLogStub.resetHistory(); + + options = { cwd: path.join('path\\to\\workspace', 'debug', 'path') }; + logger.logProcess(`"${path.join('path', 'to', 'workspace', 'test')}" "--foo" "--bar"`, undefined, options); + + sinon.assert.calledWithExactly(traceLogStub, `> ".${path.sep}test" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `cwd: .${path.sep + path.join('debug', 'path')}`); }); 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' - ); + sinon.assert.calledWithExactly(traceLogStub, `> ${path.join('~', 'test')} --foo --bar`); }); 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' - ); + sinon.assert.calledWithExactly(traceLogStub, `> ${path.join('~', 'test')} --foo --bar`); }); }); diff --git a/src/test/common/process/proc.exec.test.ts b/src/test/common/process/proc.exec.test.ts index 816a6db55ac4..21351d811b63 100644 --- a/src/test/common/process/proc.exec.test.ts +++ b/src/test/common/process/proc.exec.test.ts @@ -6,16 +6,15 @@ 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 { isOs, isPythonVersion } from '../../common'; +import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from './../../initialize'; -use(chaiAsPromised); +use(chaiAsPromised.default); -// tslint:disable-next-line:max-func-body-length suite('ProcessService Observable', () => { let pythonPath: string; suiteSetup(() => { @@ -26,7 +25,7 @@ suite('ProcessService Observable', () => { teardown(initialize); test('exec should output print statements', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const printOutput = '1234'; const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`]); @@ -35,15 +34,24 @@ suite('ProcessService Observable', () => { expect(result.stderr).to.equal(undefined, 'stderr not undefined'); }); + test('When using worker threads, exec should output print statements', async () => { + const procService = new ProcessService(); + const printOutput = '1234'; + const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`], { useWorker: true }); + + 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 procService = new ProcessService(); const printOutput = 'öä'; const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`]); @@ -53,9 +61,8 @@ suite('ProcessService Observable', () => { }); 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -65,7 +72,7 @@ suite('ProcessService Observable', () => { 'print("2")', 'sys.stdout.flush()', 'time.sleep(1)', - 'print("3")' + 'print("3")', ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['1', '2', '3']; @@ -80,9 +87,8 @@ suite('ProcessService Observable', () => { }); 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -92,7 +98,7 @@ suite('ProcessService Observable', () => { 'sys.stdout.write("2")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stdout.write("3")' + 'sys.stdout.write("3")', ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['123']; @@ -107,9 +113,8 @@ suite('ProcessService Observable', () => { }); 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -117,13 +122,13 @@ suite('ProcessService Observable', () => { 'sys.stdout.flush()', 'time.sleep(10)', 'print("2")', - 'sys.stdout.flush()' + 'sys.stdout.flush()', ]; const cancellationToken = new CancellationTokenSource(); setTimeout(() => cancellationToken.cancel(), 3000); const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')], { - token: cancellationToken.token + token: cancellationToken.token, }); expect(result).not.to.be.an('undefined', 'result is undefined'); @@ -135,11 +140,11 @@ suite('ProcessService Observable', () => { 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 + test('exec should stream stdout and stderr separately and filter output using conda related markers', async function () { this.timeout(7000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ + 'print(">>>PYTHON-EXEC-OUTPUT")', 'import sys', 'import time', 'print("1")', @@ -158,7 +163,8 @@ suite('ProcessService Observable', () => { 'sys.stdout.flush()', 'time.sleep(1)', 'sys.stderr.write("c")', - 'sys.stderr.flush()' + 'sys.stderr.flush()', + 'print("<<<PYTHON-EXEC-OUTPUT")', ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); const expectedStdout = ['1', '2', '3']; @@ -178,9 +184,8 @@ suite('ProcessService Observable', () => { }); 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -200,7 +205,7 @@ suite('ProcessService Observable', () => { 'sys.stdout.flush()', 'time.sleep(1)', 'sys.stderr.write("c")', - 'sys.stderr.flush()' + 'sys.stderr.flush()', ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')], { mergeStdOutErr: true }); const expectedOutput = ['1a2b3c']; @@ -214,7 +219,7 @@ suite('ProcessService Observable', () => { }); test('exec should throw an error with stderr output', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = ['import sys', 'sys.stderr.write("a")', 'sys.stderr.flush()']; const result = procService.exec(pythonPath, ['-c', pythonCode.join(';')], { throwOnStdErr: true }); @@ -222,37 +227,51 @@ suite('ProcessService Observable', () => { }); test('exec should throw an error when spawn file not found', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); 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 procService = new ProcessService(); 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()); + test('shellExec should be able to run python and filter output using conda related markers', async () => { + const procService = new ProcessService(); + const printOutput = '1234'; + const result = await procService.shellExec( + `"${pythonPath}" -c "print('>>>PYTHON-EXEC-OUTPUT');print('${printOutput}');print('<<<PYTHON-EXEC-OUTPUT')"`, + ); + + 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('When using worker threads, shellExec should be able to run python and filter output using conda related markers', async () => { + const procService = new ProcessService(); const printOutput = '1234'; - const result = await procService.shellExec(`"${pythonPath}" -c "print('${printOutput}')"`); + const result = await procService.shellExec( + `"${pythonPath}" -c "print('>>>PYTHON-EXEC-OUTPUT');print('${printOutput}');print('<<<PYTHON-EXEC-OUTPUT')"`, + { useWorker: true }, + ); 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 procService = new ProcessService(); 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); + const procService = new ProcessService(process.env); let result = await procService.exec(pythonPath, ['-c', `import os;print(os.environ.get("MY_TEST_VARIABLE"))`], { - extraVariables: { MY_TEST_VARIABLE: 'foo' } + extraVariables: { MY_TEST_VARIABLE: 'foo' }, }); expect(result).not.to.be.an('undefined', 'result is undefined'); diff --git a/src/test/common/process/proc.observable.test.ts b/src/test/common/process/proc.observable.test.ts index 8ebfdec3148b..debae38cc6eb 100644 --- a/src/test/common/process/proc.observable.test.ts +++ b/src/test/common/process/proc.observable.test.ts @@ -4,15 +4,14 @@ 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 { isOs, OSType } from '../../common'; +import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from './../../initialize'; -use(chaiAsPromised); +use(chaiAsPromised.default); -// tslint:disable-next-line:max-func-body-length suite('ProcessService', () => { let pythonPath: string; suiteSetup(() => { @@ -23,9 +22,8 @@ suite('ProcessService', () => { 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -37,7 +35,7 @@ suite('ProcessService', () => { 'time.sleep(2)', 'print("3")', 'sys.stdout.flush()', - 'time.sleep(2)' + 'time.sleep(2)', ]; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['1', '2', '3']; @@ -58,19 +56,18 @@ suite('ProcessService', () => { } }, done, - 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -82,7 +79,7 @@ suite('ProcessService', () => { 'time.sleep(2)', 'sys.stdout.write("3")', 'sys.stdout.flush()', - 'time.sleep(2)' + 'time.sleep(2)', ]; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['1', '2', '3']; @@ -103,14 +100,13 @@ suite('ProcessService', () => { } }, done, - 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -119,11 +115,11 @@ suite('ProcessService', () => { 'time.sleep(10)', 'print("2")', 'sys.stdout.flush()', - 'time.sleep(2)' + 'time.sleep(2)', ]; const cancellationToken = new CancellationTokenSource(); const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { - token: cancellationToken.token + token: cancellationToken.token, }); const def = createDeferred(); @@ -150,14 +146,13 @@ suite('ProcessService', () => { } 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 procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -166,11 +161,11 @@ suite('ProcessService', () => { 'time.sleep(10)', 'print("2")', 'sys.stdout.flush()', - 'time.sleep(2)' + 'time.sleep(2)', ]; const cancellationToken = new CancellationTokenSource(); const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { - token: cancellationToken.token + token: cancellationToken.token, }); let procKilled = false; @@ -195,15 +190,15 @@ suite('ProcessService', () => { () => { 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 + test('execObservable should stream stdout and stderr separately and removes markers related to conda run', function (done) { this.timeout(20000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ + 'print(">>>PYTHON-EXEC-OUTPUT")', 'import sys', 'import time', 'print("1")', @@ -223,7 +218,8 @@ suite('ProcessService', () => { 'time.sleep(2)', 'sys.stderr.write("c")', 'sys.stderr.flush()', - 'time.sleep(2)' + 'time.sleep(2)', + 'print("<<<PYTHON-EXEC-OUTPUT")', ]; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); const outputs = [ @@ -232,7 +228,7 @@ suite('ProcessService', () => { { out: '2', source: 'stdout' }, { out: 'b', source: 'stderr' }, { out: '3', source: 'stdout' }, - { out: 'c', source: 'stderr' } + { out: 'c', source: 'stderr' }, ]; expect(result).not.to.be.an('undefined', 'result is undefined'); @@ -249,20 +245,18 @@ suite('ProcessService', () => { expect(output.source).to.be.equal(expectedOutput.source, 'Expected sopurce is incorrect'); }, done, - 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 procService = new ProcessService(); const pythonCode = ['import sys', 'sys.stderr.write("a")', 'sys.stderr.flush()']; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { throwOnStdErr: true }); @@ -277,12 +271,12 @@ suite('ProcessService', () => { }, () => { 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 procService = new ProcessService(); const result = procService.execObservable(Date.now().toString(), []); expect(result).not.to.be.an('undefined', 'result is undefined.'); @@ -296,12 +290,12 @@ suite('ProcessService', () => { }, () => { 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 procService = new ProcessService(); const result = procService.execObservable(pythonPath, ['-c', 'import sys', 'sys.exit()']); expect(result).not.to.be.an('undefined', 'result is undefined.'); @@ -310,7 +304,7 @@ suite('ProcessService', () => { done(`Output received, when we\'re not expecting any, ${JSON.stringify(output)}`); }, done, - done + done, ); }); }); diff --git a/src/test/common/process/proc.unit.test.ts b/src/test/common/process/proc.unit.test.ts index 0f1f8fa1dc3a..38cf450bef57 100644 --- a/src/test/common/process/proc.unit.test.ts +++ b/src/test/common/process/proc.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -17,7 +15,6 @@ interface IProcData { } suite('Process - Process Service', function () { - // tslint:disable-next-line:no-invalid-this this.timeout(5000); const procsToKill: IProcData[] = []; teardown(() => { @@ -39,14 +36,18 @@ suite('Process - Process Service', function () { test('Process is killed', async () => { const proc = spawnProc(); - - ProcessService.kill(proc.proc.pid); + expect(proc.proc.pid !== undefined).to.equal(true, 'invalid pid'); + if (proc.proc.pid) { + 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'); + expect(proc.proc.pid !== undefined).to.equal(true, 'invalid pid'); + if (proc.proc.pid) { + 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 index 5c124ebab808..5adcdeccecfd 100644 --- a/src/test/common/process/processFactory.unit.test.ts +++ b/src/test/common/process/processFactory.unit.test.ts @@ -5,11 +5,10 @@ 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 { 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'; @@ -17,13 +16,11 @@ import { IEnvironmentVariablesProvider } from '../../../client/common/variables/ 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(); @@ -31,14 +28,13 @@ suite('Process - ProcessServiceFactory', () => { when( processService.on('exec', () => { return; - }) + }), ).thenReturn(processService); disposableRegistry = []; factory = new ProcessServiceFactory( instance(envVariablesProvider), instance(processLogger), - instance(bufferDecoder), - disposableRegistry + disposableRegistry, ); }); diff --git a/src/test/common/process/pythonDaemon.functional.test.ts b/src/test/common/process/pythonDaemon.functional.test.ts deleted file mode 100644 index f7ac134e0c08..000000000000 --- a/src/test/common/process/pythonDaemon.functional.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -// 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 deleted file mode 100644 index a0eb331178b5..000000000000 --- a/src/test/common/process/pythonDaemonPool.functional.test.ts +++ /dev/null @@ -1,472 +0,0 @@ -// 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 deleted file mode 100644 index faa29f075530..000000000000 --- a/src/test/common/process/pythonDaemonPool.unit.test.ts +++ /dev/null @@ -1,313 +0,0 @@ -// 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 index a4035e1c1af8..a2cca66d08be 100644 --- a/src/test/common/process/pythonEnvironment.unit.test.ts +++ b/src/test/common/process/pythonEnvironment.unit.test.ts @@ -1,26 +1,23 @@ // 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 * as sinon from 'sinon'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { IFileSystem } from '../../../client/common/platform/types'; import { createCondaEnv, createPythonEnv, - createWindowsStoreEnv + createMicrosoftStoreEnv, } 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'); +import { Conda } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { OUTPUT_MARKER_SCRIPT } from '../../../client/common/process/internal/scripts'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('PythonEnvironment', () => { let processService: TypeMoq.IMock<IProcessService>; @@ -34,40 +31,80 @@ suite('PythonEnvironment', () => { test('getInterpreterInformation should return an object if the python path is valid', async () => { const json = { - versionInfo: [3, 7, 5, 'candidate'], + 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 + is64Bit: true, }; processService .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + .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'), + version: new SemVer('3.7.5-candidate1'), sysPrefix: json.sysPrefix, - sysVersion: undefined + 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 5 items', async () => { + const json = { + versionInfo: [3, 7, 5, 'alpha'], + sysPrefix: '/path/of/sysprefix/versions/3.7.5a1', + version: '3.7.5a1 (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-alpha'), + 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 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 + is64Bit: true, }; processService .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); const result = await env.getInterpreterInformation(); @@ -76,12 +113,12 @@ suite('PythonEnvironment', () => { path: pythonPath, version: new SemVer('3.7.5'), sysPrefix: json.sysPrefix, - sysVersion: undefined + sysVersion: undefined, }; expect(result).to.deep.equal( expectedResult, - 'Incorrect value returned by getInterpreterInformation() with truncated versionInfo.' + 'Incorrect value returned by getInterpreterInformation() with truncated versionInfo.', ); }); @@ -90,12 +127,16 @@ suite('PythonEnvironment', () => { 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 + is64Bit: false, }; processService .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); const result = await env.getInterpreterInformation(); @@ -104,19 +145,19 @@ suite('PythonEnvironment', () => { path: pythonPath, version: new SemVer('3.7.5-candidate'), sysPrefix: json.sysPrefix, - sysVersion: undefined + sysVersion: undefined, }; expect(result).to.deep.equal( expectedResult, - 'Incorrect value returned by getInterpreterInformation() for x86b architecture.' + '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); @@ -124,7 +165,7 @@ suite('PythonEnvironment', () => { expect(result).to.equal( undefined, - 'getInterpreterInfo() should return undefined because interpreterInfo timed out.' + 'getInterpreterInfo() should return undefined because interpreterInfo timed out.', ); }); @@ -140,7 +181,7 @@ suite('PythonEnvironment', () => { }); test('getExecutablePath should return pythonPath if pythonPath is a file', async () => { - fileSystem.setup((f) => f.fileExists(pythonPath)).returns(() => Promise.resolve(true)); + fileSystem.setup((f) => f.pathExists(pythonPath)).returns(() => Promise.resolve(true)); const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); const result = await env.getExecutablePath(); @@ -150,10 +191,9 @@ suite('PythonEnvironment', () => { 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)']; + fileSystem.setup((f) => f.pathExists(pythonPath)).returns(() => Promise.resolve(false)); processService - .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .setup((p) => p.shellExec(`${pythonPath} -c "import sys;print(sys.executable)"`, TypeMoq.It.isAny())) .returns(() => Promise.resolve({ stdout: executablePath })); const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); @@ -162,23 +202,22 @@ suite('PythonEnvironment', () => { 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 () => { + test('getExecutablePath should return `undefined` 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)']; + fileSystem.setup((f) => f.pathExists(pythonPath)).returns(() => Promise.resolve(false)); processService - .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .setup((p) => p.shellExec(`${pythonPath} -c "import sys;print(sys.executable)"`, TypeMoq.It.isAny())) .returns(() => Promise.reject(new StdErrError(stderr))); const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); - const result = env.getExecutablePath(); + const result = await env.getExecutablePath(); - await expect(result).to.eventually.be.rejectedWith(stderr); + expect(result).to.be.equal(undefined); }); test('isModuleInstalled should call processService.exec()', async () => { const moduleName = 'foo'; - const argv = [isolated, '-c', `import ${moduleName}`]; + const argv = ['-c', `import ${moduleName}`]; processService .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) .returns(() => Promise.resolve({ stdout: '' })) @@ -192,7 +231,7 @@ suite('PythonEnvironment', () => { test('isModuleInstalled should return true when processService.exec() succeeds', async () => { const moduleName = 'foo'; - const argv = [isolated, '-c', `import ${moduleName}`]; + const argv = ['-c', `import ${moduleName}`]; processService .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) .returns(() => Promise.resolve({ stdout: '' })); @@ -205,7 +244,7 @@ suite('PythonEnvironment', () => { test('isModuleInstalled should return false when processService.exec() throws', async () => { const moduleName = 'foo'; - const argv = [isolated, '-c', `import ${moduleName}`]; + const argv = ['-c', `import ${moduleName}`]; processService .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) .returns(() => Promise.reject(new StdErrError('bar'))); @@ -224,7 +263,7 @@ suite('PythonEnvironment', () => { expect(result).to.deep.equal( { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }, - 'getExecutionInfo should return pythonPath and the command and execution arguments as is' + 'getExecutionInfo should return pythonPath and the command and execution arguments as is', ); }); }); @@ -237,60 +276,74 @@ suite('CondaEnvironment', () => { const condaFile = 'path/to/conda'; setup(() => { + sinon.stub(Conda, 'getConda').resolves(new Conda(condaFile)); + sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); 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', () => { + teardown(() => sinon.restore()); + + test('getExecutionInfo with a named environment should return execution info using the environment path', async () => { const condaInfo = { name: 'foo', path: 'bar' }; - const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); - const result = env.getExecutionInfo(args); + const result = env?.getExecutionInfo(args, pythonPath); expect(result).to.deep.equal({ command: condaFile, - args: ['run', '-n', condaInfo.name, 'python', ...args], - python: [condaFile, 'run', '-n', condaInfo.name, 'python'], - pythonExecutable: 'python' + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + pythonExecutable: pythonPath, }); }); - test('getExecutionInfo with a non-named environment should return execution info using the environment path', () => { + test('getExecutionInfo with a non-named environment should return execution info using the environment path', async () => { const condaInfo = { name: '', path: 'bar' }; - const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); - const result = env.getExecutionInfo(args); + const result = env?.getExecutionInfo(args, pythonPath); expect(result).to.deep.equal({ command: condaFile, - args: ['run', '-p', condaInfo.path, 'python', ...args], - python: [condaFile, 'run', '-p', condaInfo.path, 'python'], - pythonExecutable: 'python' + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + pythonExecutable: pythonPath, }); }); - test('getExecutionObservableInfo with a named environment should return execution info using pythonPath only', () => { - const expected = { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }; + test('getExecutionObservableInfo with a named environment should return execution info using conda full path with the path', async () => { const condaInfo = { name: 'foo', path: 'bar' }; - const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + const expected = { + command: condaFile, + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + pythonExecutable: pythonPath, + }; + const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); - const result = env.getExecutionObservableInfo(args); + const result = env?.getExecutionObservableInfo(args, pythonPath); 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 }; + test('getExecutionObservableInfo with a non-named environment should return execution info using conda full path', async () => { const condaInfo = { name: '', path: 'bar' }; - const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + const expected = { + command: condaFile, + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + pythonExecutable: pythonPath, + }; + const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); - const result = env.getExecutionObservableInfo(args); + const result = env?.getExecutionObservableInfo(args, pythonPath); expect(result).to.deep.equal(expected); }); }); -suite('WindowsStoreEnvironment', () => { +suite('MicrosoftStoreEnvironment', () => { let processService: TypeMoq.IMock<IProcessService>; const pythonPath = 'foo'; @@ -298,8 +351,8 @@ suite('WindowsStoreEnvironment', () => { 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); + test('Should return pythonPath if it is the path to the microsoft store interpreter', async () => { + const env = createMicrosoftStoreEnv(pythonPath, processService.object); const executablePath = await env.getExecutablePath(); diff --git a/src/test/common/process/pythonExecutionFactory.unit.test.ts b/src/test/common/process/pythonExecutionFactory.unit.test.ts index a6f6b317c3d7..0981c59e78bb 100644 --- a/src/test/common/process/pythonExecutionFactory.unit.test.ts +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -1,9 +1,11 @@ // 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 { SemVer } from 'semver'; import * as sinon from 'sinon'; import { anyString, anything, instance, mock, reset, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; @@ -11,44 +13,41 @@ 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 { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; import { - ExecutionFactoryCreationOptions, - IBufferDecoder, IProcessLogger, IProcessService, IProcessServiceFactory, - IPythonExecutionService + IPythonExecutionService, } from '../../../client/common/process/types'; -import { IConfigurationService, IDisposableRegistry } from '../../../client/common/types'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } 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 { + IActivatedEnvironmentLaunch, + IComponentAdapter, + 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 { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; - -// tslint:disable:no-any max-func-body-length chai-vague-errors +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { IInterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection/types'; +import { Conda, CONDA_RUN_VERSION } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; +import * as pixi from '../../../client/pythonEnvironments/common/environmentManagers/pixi'; -const pythonInterpreter: PythonInterpreter = { +const pythonInterpreter: PythonEnvironment = { path: '/foo/bar/python.exe', version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, }; -function title(resource?: Uri, interpreter?: PythonInterpreter) { +function title(resource?: Uri, interpreter?: PythonEnvironment) { return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; } @@ -56,7 +55,7 @@ async function verifyCreateActivated( factory: PythonExecutionFactory, activationHelper: IEnvironmentActivationService, resource?: Uri, - interpreter?: PythonInterpreter + interpreter?: PythonEnvironment, ): Promise<IPythonExecutionService> { when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve(); @@ -72,69 +71,97 @@ suite('Process - PythonExecutionFactory', () => { { resource: undefined, interpreter: undefined }, { resource: undefined, interpreter: pythonInterpreter }, { resource: Uri.parse('x'), interpreter: undefined }, - { resource: Uri.parse('x'), interpreter: pythonInterpreter } + { resource: Uri.parse('x'), interpreter: pythonInterpreter }, ].forEach((item) => { - const resource = item.resource; - const interpreter = item.interpreter; + const { resource } = item; + const { interpreter } = item; suite(title(resource, interpreter), () => { let factory: PythonExecutionFactory; let activationHelper: IEnvironmentActivationService; - let bufferDecoder: IBufferDecoder; + let activatedEnvironmentLaunch: IActivatedEnvironmentLaunch; let processFactory: IProcessServiceFactory; let configService: IConfigurationService; - let condaService: ICondaService; let processLogger: IProcessLogger; let processService: typemoq.IMock<IProcessService>; - let windowsStoreInterpreter: IWindowsStoreInterpreter; let interpreterService: IInterpreterService; + let pyenvs: IComponentAdapter; let executionService: typemoq.IMock<IPythonExecutionService>; - let platformService: IPlatformService; + let autoSelection: IInterpreterAutoSelectionService; + let interpreterPathExpHelper: IInterpreterPathService; + let getPixiEnvironmentFromInterpreterStub: sinon.SinonStub; + let getPixiStub: sinon.SinonStub; + const pythonPath = 'path/to/python'; setup(() => { - bufferDecoder = mock(BufferDecoder); + sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); + sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); + + getPixiEnvironmentFromInterpreterStub = sinon.stub(pixi, 'getPixiEnvironmentFromInterpreter'); + getPixiEnvironmentFromInterpreterStub.resolves(undefined); + + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + activationHelper = mock(EnvironmentActivationService); processFactory = mock(ProcessServiceFactory); configService = mock(ConfigurationService); - condaService = mock(CondaService); processLogger = mock(ProcessLogger); - platformService = mock<IPlatformService>(); - windowsStoreInterpreter = mock(WindowsStoreInterpreter); + autoSelection = mock<IInterpreterAutoSelectionService>(); + interpreterPathExpHelper = mock<IInterpreterPathService>(); + when(interpreterPathExpHelper.get(anything())).thenReturn('selected interpreter path'); + + pyenvs = mock<IComponentAdapter>(); + when(pyenvs.isMicrosoftStoreInterpreter(anyString())).thenResolve(true); + executionService = typemoq.Mock.ofType<IPythonExecutionService>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any executionService.setup((p: any) => p.then).returns(() => undefined); when(processLogger.logProcess('', [], {})).thenReturn(); processService = typemoq.Mock.ofType<IProcessService>(); processService .setup((p) => p.on('exec', () => { - return; - }) + /** No body */ + }), ) .returns(() => processService.object); + // eslint-disable-next-line @typescript-eslint/no-explicit-any processService.setup((p: any) => p.then).returns(() => undefined); interpreterService = mock(InterpreterService); when(interpreterService.getInterpreterDetails(anything())).thenResolve({ - version: { major: 3 } + version: { major: 3 }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } 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) + instance(interpreterService), + ); + activatedEnvironmentLaunch = mock<IActivatedEnvironmentLaunch>(); + when(activatedEnvironmentLaunch.selectIfLaunchedViaActivatedEnv()).thenResolve(); + when(serviceContainer.get<IActivatedEnvironmentLaunch>(IActivatedEnvironmentLaunch)).thenReturn( + instance(activatedEnvironmentLaunch), ); + when(serviceContainer.get<IComponentAdapter>(IComponentAdapter)).thenReturn(instance(pyenvs)); when(serviceContainer.tryGet<IInterpreterService>(IInterpreterService)).thenReturn( - instance(interpreterService) + instance(interpreterService), + ); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( + instance(configService), ); factory = new PythonExecutionFactory( instance(serviceContainer), instance(activationHelper), instance(processFactory), instance(configService), - instance(condaService), - instance(bufferDecoder), - instance(windowsStoreInterpreter), - instance(platformService) + instance(pyenvs), + instance(autoSelection), + instance(interpreterPathExpHelper), ); }); + teardown(() => sinon.restore()); + test('Ensure PythonExecutionService is created', async () => { const pythonSettings = mock(PythonSettings); when(processFactory.create(resource)).thenResolve(processService.object); @@ -148,8 +175,57 @@ suite('Process - PythonExecutionFactory', () => { verify(processFactory.create(resource)).once(); verify(pythonSettings.pythonPath).once(); }); + + test('If interpreter is explicitly set to `python`, ensure we use it', async () => { + const pythonSettings = mock(PythonSettings); + when(processFactory.create(resource)).thenResolve(processService.object); + when(activationHelper.getActivatedEnvironmentVariables(resource)).thenResolve({ x: '1' }); + reset(interpreterPathExpHelper); + when(interpreterPathExpHelper.get(anything())).thenReturn('python'); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + const service = await factory.create({ resource, pythonPath: 'python' }); + + expect(service).to.not.equal(undefined); + verify(autoSelection.autoSelectInterpreter(anything())).once(); + }); + + test('Otherwise if interpreter is explicitly set, ensure we use it', async () => { + const pythonSettings = mock(PythonSettings); + when(processFactory.create(resource)).thenResolve(processService.object); + when(activationHelper.getActivatedEnvironmentVariables(resource)).thenResolve({ x: '1' }); + reset(interpreterPathExpHelper); + when(interpreterPathExpHelper.get(anything())).thenReturn('python'); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + const service = await factory.create({ resource, pythonPath: 'HELLO' }); + + expect(service).to.not.equal(undefined); + verify(pyenvs.isMicrosoftStoreInterpreter('HELLO')).once(); + verify(pythonSettings.pythonPath).never(); + }); + + test('If no interpreter is explicitly set, ensure we autoselect before 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'); + reset(interpreterPathExpHelper); + when(interpreterPathExpHelper.get(anything())).thenReturn('python'); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + const service = await factory.create({ resource }); + + expect(service).to.not.equal(undefined); + verify(autoSelection.autoSelectInterpreter(anything())).once(); + 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); @@ -158,17 +234,17 @@ suite('Process - PythonExecutionFactory', () => { let createInvoked = false; const mockExecService = 'something'; - factory.create = async (_options: ExecutionFactoryCreationOptions) => { + factory.create = async () => { createInvoked = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any return Promise.resolve((mockExecService as any) as IPythonExecutionService); }; const service = await verifyCreateActivated(factory, activationHelper, resource, interpreter); assert.deepEqual(service, mockExecService); - assert.equal(createInvoked, true); + assert.strictEqual(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); @@ -177,26 +253,28 @@ suite('Process - PythonExecutionFactory', () => { let createInvoked = false; const mockExecService = 'something'; - factory.create = async (_options: ExecutionFactoryCreationOptions) => { + factory.create = async () => { createInvoked = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any return Promise.resolve((mockExecService as any) as IPythonExecutionService); }; const service = await verifyCreateActivated(factory, activationHelper, resource, interpreter); assert.deepEqual(service, mockExecService); - assert.equal(createInvoked, true); + assert.strictEqual(createInvoked, true); }); test('PythonExecutionService is created', async () => { let createInvoked = false; const mockExecService = 'something'; - factory.create = async (_options: ExecutionFactoryCreationOptions) => { + factory.create = async () => { createInvoked = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any return Promise.resolve((mockExecService as any) as IPythonExecutionService); }; const pythonSettings = mock(PythonSettings); when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ - x: '1' + x: '1', }); when(pythonSettings.pythonPath).thenReturn('HELLO'); when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); @@ -207,319 +285,138 @@ suite('Process - PythonExecutionFactory', () => { if (!interpreter) { verify(pythonSettings.pythonPath).once(); } - assert.equal(createInvoked, false); + assert.strictEqual(createInvoked, false); }); - test("Ensure `create` returns a WindowsStorePythonProcess instance if it's a windows store intepreter path", async () => { - const pythonPath = 'path/to/python'; + test('Ensure `create` returns a CondaExecutionService instance if createCondaExecutionService() returns a valid object', async () => { 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(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({ + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + when(pyenvs.getCondaEnvironment(pythonPath)).thenResolve({ name: 'foo', - path: 'path/to/foo/env' + 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(); + verify(pyenvs.getCondaEnvironment(pythonPath)).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'; + test('Ensure `create` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async () => { 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); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(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(); + verify(pyenvs.getCondaEnvironment(pythonPath)).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'; + test('Ensure `createActivatedEnvironment` returns a CondaExecutionService instance if createCondaExecutionService() returns a valid object', async () => { 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' + x: '1', }); when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); - when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); - when(condaService.getCondaEnvironment(anyString())).thenResolve({ + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + when(pyenvs.getCondaEnvironment(anyString())).thenResolve({ name: 'foo', - path: 'path/to/foo/env' + 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(); + verify(pyenvs.getCondaEnvironment(pythonPath)).once(); } else { - // @ts-ignore - verify(condaService.getCondaEnvironment(interpreter.path)).once(); + verify(pyenvs.getCondaEnvironment(interpreter!.path)).once(); } + expect(getPixiEnvironmentFromInterpreterStub.notCalled).to.be.equal(true); }); - test('Ensure `createActivatedEnvironment` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async function () { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - + test('Ensure `createActivatedEnvironment` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async () => { let createInvoked = false; - const pythonPath = 'path/to/python'; + const mockExecService = 'mockService'; - factory.create = async (_options: ExecutionFactoryCreationOptions) => { + factory.create = async () => { createInvoked = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any return Promise.resolve((mockExecService as any) as IPythonExecutionService); }; const pythonSettings = mock(PythonSettings); when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ - x: '1' + x: '1', }); when(pythonSettings.pythonPath).thenReturn(pythonPath); when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); - when(condaService.getCondaVersion()).thenResolve(new SemVer('1.0.0')); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(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); + assert.strictEqual(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({ + when(pyenvs.getCondaEnvironment(pythonPath)).thenResolve({ name: 'foo', - path: 'path/to/foo/env' + path: 'path/to/foo/env', }); - when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); - when(condaService.getCondaFile()).thenResolve('conda'); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); - const result = await factory.createCondaExecutionService(pythonPath, undefined, resource); + const result = await factory.createCondaExecutionService(pythonPath, processService.object); expect(result).to.not.equal(undefined); - verify(processFactory.create(resource)).once(); - verify(condaService.getCondaVersion()).once(); - verify(condaService.getCondaEnvironment(pythonPath)).once(); - verify(condaService.getCondaFile()).once(); + verify(pyenvs.getCondaEnvironment(pythonPath)).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)); + when(pyenvs.getCondaEnvironment(pythonPath)).thenResolve(undefined); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(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' + 'createCondaExecutionService should return undefined if not in a conda environment', ); - verify(condaService.getCondaVersion()).once(); - verify(condaService.getCondaEnvironment(pythonPath)).once(); - verify(condaService.getCondaFile()).once(); + verify(pyenvs.getCondaEnvironment(pythonPath)).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')); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(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' + '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); + verify(pyenvs.getCondaEnvironment(pythonPath)).once(); }); }); }); diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index 7df62d4a805a..fc4fbf5328a9 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -6,25 +6,24 @@ 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 * as fs from '../../../client/common/platform/fs-paths'; 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'; +import { initializeExternalDependencies } from '../../../client/pythonEnvironments/common/externalDependencies'; +import { clearPythonPathInWorkspaceFolder } from '../../common'; +import { getExtensionSettings } from '../../extensionSettings'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../../initialize'; -use(chaiAsPromised); +use(chaiAsPromised.default); 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; @@ -32,13 +31,13 @@ suite('PythonExecutableService', () => { 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 () => { + initializeExternalDependencies(serviceContainer); configService = serviceContainer.get<IConfigurationService>(IConfigurationService); pythonExecFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); @@ -60,35 +59,28 @@ suite('PythonExecutableService', () => { 'envFile', 'someInvalidFile.env', workspace4PyFile, - ConfigurationTarget.WorkspaceFolder + 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 + 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(); - } + }).timeout(TEST_TIMEOUT * 3); + test('Importing with a valid PYTHONPATH from .env file should succeed', async () => { await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); - const promise = pythonExecService.exec([workspace4PyFile.fsPath], { + const result = await pythonExecService.exec([workspace4PyFile.fsPath], { cwd: path.dirname(workspace4PyFile.fsPath), - throwOnStdErr: true + throwOnStdErr: true, }); - await expect(promise).to.eventually.have.property('stdout', `Hello${EOL}`); - }); + expect(result.stdout.startsWith('Hello')).to.be.equals(true); + }).timeout(TEST_TIMEOUT * 3); test("Known modules such as 'os' and 'sys' should be deemed 'installed'", async () => { const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); @@ -96,7 +88,7 @@ suite('PythonExecutableService', () => { 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'); - }); + }).timeout(TEST_TIMEOUT * 3); test("Unknown modules such as 'xyzabc123' be deemed 'not installed'", async () => { const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); @@ -104,12 +96,12 @@ suite('PythonExecutableService', () => { const randomModuleIsInstalled = pythonExecService.isModuleInstalled(randomModuleName); await expect(randomModuleIsInstalled).to.eventually.equal( false, - `Random module '${randomModuleName}' is installed` + `Random module '${randomModuleName}' is installed`, ); - }); + }).timeout(TEST_TIMEOUT * 3); test('Ensure correct path to executable is returned', async () => { - const pythonPath = getExtensionSettings(workspace4Path).pythonPath; + const { pythonPath } = getExtensionSettings(workspace4Path); let expectedExecutablePath: string; if (await fs.pathExists(pythonPath)) { expectedExecutablePath = pythonPath; @@ -123,5 +115,5 @@ suite('PythonExecutableService', () => { const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const executablePath = await pythonExecService.getExecutablePath(); expect(executablePath).to.equal(expectedExecutablePath, 'Executable paths are not the same'); - }); + }).timeout(TEST_TIMEOUT * 3); }); diff --git a/src/test/common/process/pythonProcess.unit.test.ts b/src/test/common/process/pythonProcess.unit.test.ts index 646ad8473c05..7382fc9f9869 100644 --- a/src/test/common/process/pythonProcess.unit.test.ts +++ b/src/test/common/process/pythonProcess.unit.test.ts @@ -3,20 +3,15 @@ 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'); +use(chaiAsPromised.default); -use(chaiAsPromised); - -// tslint:disable-next-line: max-func-body-length suite('PythonProcessService', () => { let processService: TypeMoq.IMock<IProcessService>; let fileSystem: TypeMoq.IMock<IFileSystem>; @@ -32,11 +27,11 @@ suite('PythonProcessService', () => { 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); @@ -51,15 +46,15 @@ suite('PythonProcessService', () => { test('execModuleObservable should call processService.execObservable with the -m argument', () => { const args = ['-a', 'b', '-c']; const moduleName = 'foo'; - const expectedArgs = [isolated, moduleName, ...args]; + const expectedArgs = ['-m', 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); @@ -88,7 +83,7 @@ suite('PythonProcessService', () => { 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 expectedArgs = ['-m', moduleName, ...args]; const options = {}; const stdout = 'bar'; processService @@ -106,13 +101,13 @@ suite('PythonProcessService', () => { 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 expectedArgs = ['-m', 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 })) + .setup((p) => p.exec(pythonPath, ['-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); diff --git a/src/test/common/process/pythonToolService.unit.test.ts b/src/test/common/process/pythonToolService.unit.test.ts index ff83e1ead8f7..bef199ce223a 100644 --- a/src/test/common/process/pythonToolService.unit.test.ts +++ b/src/test/common/process/pythonToolService.unit.test.ts @@ -18,27 +18,26 @@ import { IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService, - ObservableExecutionResult + ObservableExecutionResult, } from '../../../client/common/process/types'; import { ExecutionInfo } from '../../../client/common/types'; import { ServiceContainer } from '../../../client/ioc/container'; import { noop } from '../../core'; -use(chaiAsPromised); +use(chaiAsPromised.default); -// 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' + stdout: 'output', }; let pythonService: IPythonExecutionService; @@ -53,7 +52,7 @@ suite('Process - Python tool execution service', () => { 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); @@ -68,7 +67,7 @@ suite('Process - Python tool execution service', () => { const serviceContainer = mock(ServiceContainer); when(serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory)).thenReturn( - instance(executionFactory) + instance(executionFactory), ); when(serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory)).thenReturn(instance(processFactory)); @@ -80,7 +79,7 @@ suite('Process - Python tool execution service', () => { const executionInfo: ExecutionInfo = { execPath: 'foo', moduleName: 'moduleOne', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const promise = executionService.execObservable(executionInfo, options, resource); @@ -93,7 +92,7 @@ suite('Process - Python tool execution service', () => { const executionInfo: ExecutionInfo = { execPath: 'foo', moduleName: 'moduleOne', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const result = await executionService.execObservable(executionInfo, options, resource); @@ -107,7 +106,7 @@ suite('Process - Python tool execution service', () => { const executionInfo: ExecutionInfo = { execPath: 'foo', moduleName: '', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const result = await executionService.execObservable(executionInfo, options, resource); @@ -120,7 +119,7 @@ suite('Process - Python tool execution service', () => { const options = {}; const executionInfo: ExecutionInfo = { execPath: 'foo', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const result = await executionService.execObservable(executionInfo, options, resource); @@ -134,7 +133,7 @@ suite('Process - Python tool execution service', () => { const executionInfo: ExecutionInfo = { execPath: 'foo', moduleName: 'moduleOne', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const promise = executionService.exec(executionInfo, options, resource); @@ -147,7 +146,7 @@ suite('Process - Python tool execution service', () => { const executionInfo: ExecutionInfo = { execPath: 'foo', moduleName: 'moduleOne', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const result = await executionService.exec(executionInfo, options, resource); @@ -161,7 +160,7 @@ suite('Process - Python tool execution service', () => { const executionInfo: ExecutionInfo = { execPath: 'foo', moduleName: '', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const result = await executionService.exec(executionInfo, options, resource); @@ -174,7 +173,7 @@ suite('Process - Python tool execution service', () => { const options = {}; const executionInfo: ExecutionInfo = { execPath: 'foo', - args: ['-a', 'b', '-c'] + args: ['-a', 'b', '-c'], }; const result = await executionService.exec(executionInfo, options, resource); diff --git a/src/test/common/process/serviceRegistry.unit.test.ts b/src/test/common/process/serviceRegistry.unit.test.ts index 6f8b3d63d902..a0187aeedffc 100644 --- a/src/test/common/process/serviceRegistry.unit.test.ts +++ b/src/test/common/process/serviceRegistry.unit.test.ts @@ -4,16 +4,14 @@ '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 + IPythonToolExecutionService, } from '../../../client/common/process/types'; import { ServiceManager } from '../../../client/ioc/serviceManager'; import { IServiceManager } from '../../../client/ioc/types'; @@ -27,18 +25,17 @@ suite('Common Process Service Registry', () => { test('Ensure services are registered', async () => { registerTypes(instance(serviceManager)); - verify(serviceManager.addSingleton<IBufferDecoder>(IBufferDecoder, BufferDecoder)).once(); verify( - serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, ProcessServiceFactory) + serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, ProcessServiceFactory), ).once(); verify( - serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory) + serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory), ).once(); verify( serviceManager.addSingleton<IPythonToolExecutionService>( IPythonToolExecutionService, - PythonToolExecutionService - ) + PythonToolExecutionService, + ), ).once(); }); }); diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts index d40edb5e5378..9a82681625d4 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -3,11 +3,8 @@ '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'; @@ -25,37 +22,15 @@ import { IDebugService, IDocumentManager, ILanguageService, - ILiveShareApi, ITerminalManager, - IWorkspaceService + 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 { PipEnvExecutionPath } from '../../client/common/configuration/executionSettings/pipEnvExecution'; 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'; @@ -64,6 +39,7 @@ 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 { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; @@ -80,28 +56,23 @@ import { ITerminalActivator, ITerminalHelper, ITerminalServiceFactory, - TerminalActivationProviders + TerminalActivationProviders, } from '../../client/common/terminal/types'; import { - IAsyncDisposableRegistry, IBrowserService, IConfigurationService, - ICryptoUtils, ICurrentProcess, - IEditorUtils, - IExperimentsManager, IExtensions, - IFeatureDeprecationManager, - IHttpClient, IInstaller, IInterpreterPathService, IPathUtils, IPersistentStateFactory, - IRandom + IRandom, + IToolExecutionPath, + ToolExecutionPath, } 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'; @@ -130,46 +101,34 @@ suite('Common - Service Registry', () => { [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 + TerminalActivationProviders.commandPromptAndPowerShell, ], + [ITerminalActivationCommandProvider, Nushell, TerminalActivationProviders.nushell], + [IToolExecutionPath, PipEnvExecutionPath, ToolExecutionPath.pipenv], [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) - ) + typemoq.It.is((value: any) => mapping[1] === value), + ), ) .verifiable(typemoq.Times.atLeastOnce()); } else { @@ -178,8 +137,8 @@ suite('Common - Service Registry', () => { s.addSingleton( typemoq.It.isValue(mapping[0] as any), typemoq.It.isAny(), - typemoq.It.isValue(mapping[2] as any) - ) + typemoq.It.isValue(mapping[2] as any), + ), ) .callback((_, cls) => expect(cls).to.equal(mapping[1])) .verifiable(typemoq.Times.once()); diff --git a/src/test/common/socketCallbackHandler.test.ts b/src/test/common/socketCallbackHandler.test.ts index 70b5b3f04c74..5fbac0083125 100644 --- a/src/test/common/socketCallbackHandler.test.ts +++ b/src/test/common/socketCallbackHandler.test.ts @@ -1,5 +1,3 @@ -// 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'; @@ -10,10 +8,9 @@ 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'); + public static ExitCommandBytes: Buffer = Buffer.from('exit'); + public static PingBytes: Buffer = Buffer.from('ping'); } namespace ResponseCommands { @@ -36,10 +33,10 @@ class MockSocketCallbackHandler extends SocketCallbackHandler { public ping(message: string) { this.SendRawCommand(Commands.PingBytes); - const stringBuffer = new Buffer(message); + const stringBuffer = Buffer.from(message); const buffer = Buffer.concat([ - Buffer.concat([new Buffer('U'), uint64be.encode(stringBuffer.byteLength)]), - stringBuffer + Buffer.concat([Buffer.from('U'), uint64be.encode(stringBuffer.byteLength)]), + stringBuffer, ]); this.stream.Write(buffer); } @@ -105,7 +102,7 @@ class MockSocketClient { if (this.socket === undefined || this.def === undefined) { throw Error('not started'); } - this.socketStream = new SocketStream(this.socket, new Buffer('')); + this.socketStream = new SocketStream(this.socket, Buffer.from('')); this.def.resolve(); this.socket.on('error', () => {}); this.socket.on('data', (data: Buffer) => { @@ -122,7 +119,7 @@ class MockSocketClient { } cmdIdBytes.push(byte); } - const cmdId = new Buffer(cmdIdBytes).toString(); + const cmdId = Buffer.from(cmdIdBytes).toString(); const message = this.SocketStream.ReadString(); if (typeof message !== 'string') { this.SocketStream.RollBackTransaction(); @@ -132,32 +129,32 @@ class MockSocketClient { this.SocketStream.EndTransaction(); if (cmdId !== 'ping') { - this.SocketStream.Write(new Buffer(ResponseCommands.Error)); + this.SocketStream.Write(Buffer.from(ResponseCommands.Error)); const errorMessage = `Received unknown command '${cmdId}'`; const errorBuffer = Buffer.concat([ - Buffer.concat([new Buffer('A'), uint64be.encode(errorMessage.length)]), - new Buffer(errorMessage) + Buffer.concat([Buffer.from('A'), uint64be.encode(errorMessage.length)]), + Buffer.from(errorMessage), ]); this.SocketStream.Write(errorBuffer); return; } - this.SocketStream.Write(new Buffer(ResponseCommands.Pong)); + this.SocketStream.Write(Buffer.from(ResponseCommands.Pong)); - const messageBuffer = new Buffer(message); + const messageBuffer = Buffer.from(message); const pongBuffer = Buffer.concat([ - Buffer.concat([new Buffer('U'), uint64be.encode(messageBuffer.byteLength)]), - messageBuffer + Buffer.concat([Buffer.from('U'), uint64be.encode(messageBuffer.byteLength)]), + messageBuffer, ]); this.SocketStream.Write(pongBuffer); } catch (ex) { - this.SocketStream.Write(new Buffer(ResponseCommands.Error)); + this.SocketStream.Write(Buffer.from(ResponseCommands.Error)); - const errorMessage = `Fatal error in handling data at socket client. Error: ${ex.message}`; + const errorMessage = `Fatal error in handling data at socket client. Error: ${(ex as Error).message}`; const errorBuffer = Buffer.concat([ - Buffer.concat([new Buffer('A'), uint64be.encode(errorMessage.length)]), - new Buffer(errorMessage) + Buffer.concat([Buffer.from('A'), uint64be.encode(errorMessage.length)]), + Buffer.from(errorMessage), ]); this.SocketStream.Write(errorBuffer); } @@ -192,7 +189,7 @@ suite('SocketCallbackHandler', () => { expect(port).to.be.greaterThan(0); }); test('Succesfully starts with specific port', async () => { - const availablePort = await getFreePort({ host: 'localhost' }); + const availablePort = await getFreePort.default({ host: 'localhost' }); const port = await socketServer.Start({ port: availablePort, host: 'localhost' }); expect(port).to.be.equal(availablePort); }); @@ -213,7 +210,7 @@ suite('SocketCallbackHandler', () => { }); // 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)]); + const guidBuffer = Buffer.concat([Buffer.from('A'), uint64be.encode(GUID.length), Buffer.from(GUID)]); socketClient.SocketStream.Write(guidBuffer); socketClient.SocketStream.WriteInt32(PID); await def.promise; @@ -249,7 +246,7 @@ suite('SocketCallbackHandler', () => { }); // 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)]); + const guidBuffer = Buffer.concat([Buffer.from('A'), uint64be.encode(GUID.length), Buffer.from(GUID)]); socketClient.SocketStream.Write(guidBuffer); // Send the wrong pid @@ -274,7 +271,7 @@ suite('SocketCallbackHandler', () => { expect(message).to.be.equal(PING_MESSAGE); def.resolve(); } catch (ex) { - def.reject(ex); + def.reject(ex as Error); } }); callbackHandler.on('error', (actual: string, expected: string, message: string) => { @@ -284,7 +281,7 @@ suite('SocketCallbackHandler', () => { }); // 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)]); + const guidBuffer = Buffer.concat([Buffer.from('A'), uint64be.encode(GUID.length), Buffer.from(GUID)]); socketClient.SocketStream.Write(guidBuffer); // Send the wrong pid @@ -307,14 +304,14 @@ suite('SocketCallbackHandler', () => { }); // 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)]); + const guidBuffer = Buffer.concat([Buffer.from('A'), uint64be.encode(GUID.length), Buffer.from(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) + getFreePort.default({ host: 'localhost' }).then(resolve, reject), ); const port = await socketServer.Start({ port: availablePort, host: 'localhost' }); @@ -333,7 +330,7 @@ suite('SocketCallbackHandler', () => { }); // 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)]); + const guidBuffer = Buffer.concat([Buffer.from('A'), uint64be.encode(GUID.length), Buffer.from(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 index 13d3082dcd96..35420e4a614c 100644 --- a/src/test/common/socketStream.test.ts +++ b/src/test/common/socketStream.test.ts @@ -11,12 +11,12 @@ import * as assert from 'assert'; // 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 = ''; @@ -24,165 +24,169 @@ class MockSocket { 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 buffer = Buffer.from('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); + assert.strictEqual(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); + assert.strictEqual(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); + assert.strictEqual(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 buffer = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length), Buffer.from(message)]); + const stream = new SocketStream((socket as any) as net.Socket, buffer); - assert.equal(stream.ReadString(), message); + assert.strictEqual(stream.ReadString(), message); done(); }); test('Read Unicode String', (done) => { const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; const socket = new MockSocket(); - const stringBuffer = new Buffer(message); + const stringBuffer = Buffer.from(message); const buffer = Buffer.concat([ - Buffer.concat([new Buffer('U'), uint64be.encode(stringBuffer.byteLength)]), - stringBuffer + Buffer.concat([Buffer.from('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); + assert.strictEqual(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)]); + let buffer = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length), Buffer.from(message)]); // Write part of a second message - const partOfSecondMessage = Buffer.concat([new Buffer('A'), uint64be.encode(message.length)]); + const partOfSecondMessage = Buffer.concat([Buffer.from('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'); + assert.strictEqual(stream.ReadString(), message, 'First message not read properly'); stream.ReadString(); - assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + assert.strictEqual(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'); + assert.strictEqual( + 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)]); + let buffer = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length), Buffer.from(message)]); // Write part of a second message - const partOfSecondMessage = Buffer.concat([new Buffer('A'), uint64be.encode(message.length)]); + const partOfSecondMessage = Buffer.concat([Buffer.from('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'); + assert.strictEqual(stream.ReadString(), message, 'First message not read properly'); stream.ReadString(); - assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + assert.strictEqual(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'); + assert.notStrictEqual(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 buffer = Buffer.from(''); 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)); + stream.Write(Buffer.from(message)); - assert.equal(socket.dataWritten, message); + assert.strictEqual(socket.dataWritten, message); done(); }); test('Write Int32', (done) => { const num = 1234; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); 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); + assert.strictEqual(uint64be.decode(socket.rawDataWritten), num); done(); }); test('Write Int64', (done) => { const num = 9007199254740993; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); 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); + assert.strictEqual(uint64be.decode(socket.rawDataWritten), num); done(); }); test('Write Ascii String', (done) => { const message = 'Hello World'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); 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); + assert.strictEqual(socket.dataWritten, message); done(); }); test('Write Unicode String', (done) => { const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); 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); + assert.strictEqual(socket.dataWritten, message); done(); }); }); diff --git a/src/test/common/stringUtils.unit.test.ts b/src/test/common/stringUtils.unit.test.ts new file mode 100644 index 000000000000..f8b5f2947631 --- /dev/null +++ b/src/test/common/stringUtils.unit.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import '../../client/common/extensions'; +import { replaceAll } from '../../client/common/stringUtils'; + +suite('String Extensions', () => { + test('String should replace all substrings with new substring', () => { + const oldString = `foo \\ foo \\ foo`; + const expectedString = `foo \\\\ foo \\\\ foo`; + const oldString2 = `\\ foo \\ foo`; + const expectedString2 = `\\\\ foo \\\\ foo`; + const oldString3 = `\\ foo \\`; + const expectedString3 = `\\\\ foo \\\\`; + const oldString4 = `foo foo`; + const expectedString4 = `foo foo`; + expect(replaceAll(oldString, '\\', '\\\\')).to.be.equal(expectedString); + expect(replaceAll(oldString2, '\\', '\\\\')).to.be.equal(expectedString2); + expect(replaceAll(oldString3, '\\', '\\\\')).to.be.equal(expectedString3); + expect(replaceAll(oldString4, '\\', '\\\\')).to.be.equal(expectedString4); + }); +}); diff --git a/src/test/common/terminals/activation.bash.unit.test.ts b/src/test/common/terminals/activation.bash.unit.test.ts index b14415e3aab2..cd057e7be3e5 100644 --- a/src/test/common/terminals/activation.bash.unit.test.ts +++ b/src/test/common/terminals/activation.bash.unit.test.ts @@ -8,125 +8,129 @@ 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 { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; - -// tslint:disable: max-func-body-length +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Activation (bash)', () => { [ 'usr/bin/python', 'usr/bin/env with spaces/env more/python', - 'c:\\users\\windows paths\\conda\\python.exe' + '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); + [ + 'activate', + 'activate.sh', + 'activate.csh', + 'activate.fish', + 'activate.bat', + 'activate.nu', + 'Activate.ps1', + ].forEach((scriptFileName) => { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + 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); - }); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + serviceContainer + .setup((c) => c.get(IInterpreterService)) + .returns(() => interpreterService.object); + }); - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { - let isScriptFileSupported = false; + 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.wsl: + case TerminalShellType.bash: 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; + case TerminalShellType.cshell: + case TerminalShellType.fish: { + expect(supported).to.be.equal( + true, + `${shellType.name} shell not supported (it should be)`, + ); break; } default: { - isScriptFileSupported = false; + expect(supported).to.be.equal( + false, + `${shellType.name} incorrectly supported (should not be)`, + ); + // No point proceeding with other tests. + return; } } - 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 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); - 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. + 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'); - } - }); + expect(command).to.be.deep.equal( + [`source ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.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 index 56d1239caf72..ed21d7625dab 100644 --- a/src/test/common/terminals/activation.commandPrompt.unit.test.ts +++ b/src/test/common/terminals/activation.commandPrompt.unit.test.ts @@ -1,8 +1,6 @@ // 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'; @@ -10,15 +8,17 @@ 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 { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Activation (cmd/powershell)', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; [ 'c:/programfiles/python/python', 'c:/program files/python/python', - 'c:\\users\\windows paths\\conda\\python.exe' + 'c:\\users\\windows paths\\conda\\python.exe', ].forEach((pythonPath) => { const hasSpaces = pythonPath.indexOf(' ') > 0; const resource = Uri.file('a'); @@ -36,16 +36,13 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { 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>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); 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); + .setup((c) => c.get(IInterpreterService)) + .returns(() => interpreterService.object); }); getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { @@ -64,21 +61,21 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { case TerminalShellType.powershell: { expect(supported).to.be.equal( true, - `${shellType.name} shell not supported (it should be)` + `${shellType.name} shell not supported (it should be)`, ); break; } default: { expect(supported).to.be.equal( false, - `${shellType.name} incorrectly supported (should not be)` + `${shellType.name} incorrectly supported (should not be)`, ); } } }); }); }); - } + }, ); suite('and script file is activate.bat', () => { @@ -89,16 +86,13 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); platform = TypeMoq.Mock.ofType<IPlatformService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); 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 () => { @@ -115,7 +109,10 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { // 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'); + expect(commands).to.be.deep.equal( + [pathToScriptFile.fileToCommandArgumentForPythonExt()], + 'Invalid command', + ); }); test('Ensure batch files are not supported by powershell (on windows)', async () => { @@ -181,14 +178,11 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { 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); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); }); test('Ensure powershell files are not supported by command prompt', async () => { @@ -203,7 +197,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { expect(command).to.be.deep.equal( [], - 'Invalid command (running powershell files are not supported on command prompt)' + 'Invalid command (running powershell files are not supported on command prompt)', ); }); @@ -218,8 +212,8 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); expect(command).to.be.deep.equal( - [`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], - 'Invalid command' + [`& ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], + 'Invalid command', ); }); @@ -234,8 +228,8 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); expect(command).to.be.deep.equal( - [`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], - 'Invalid command' + [`& ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.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 index 6d3483cd50e0..39bf58a9a36b 100644 --- a/src/test/common/terminals/activation.conda.unit.test.ts +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -1,11 +1,8 @@ // 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'; @@ -15,7 +12,11 @@ import { IFileSystem, IPlatformService } from '../../../client/common/platform/t 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 { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; +import { + CondaActivationCommandProvider, + _getPowershellCommands, +} 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'; @@ -24,12 +25,13 @@ import { IConfigurationService, IDisposableRegistry, IPythonSettings, - ITerminalSettings + ITerminalSettings, } from '../../../client/common/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { ICondaService } from '../../../client/interpreter/contracts'; +import { IComponentAdapter, ICondaService } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { IServiceContainer } from '../../../client/ioc/types'; +import { PixiActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pixiActivationProvider'; suite('Terminal Environment Activation conda', () => { let terminalHelper: TerminalHelper; @@ -42,6 +44,7 @@ suite('Terminal Environment Activation conda', () => { let processService: TypeMoq.IMock<IProcessService>; let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; let condaService: TypeMoq.IMock<ICondaService>; + let componentAdapter: TypeMoq.IMock<IComponentAdapter>; let configService: TypeMoq.IMock<IConfigurationService>; let conda: string; let bash: ITerminalActivationCommandProvider; @@ -54,13 +57,18 @@ suite('Terminal Environment Activation conda', () => { .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) .returns(() => disposables); + componentAdapter = TypeMoq.Mock.ofType<IComponentAdapter>(); fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); platformService = TypeMoq.Mock.ofType<IPlatformService>(); processService = TypeMoq.Mock.ofType<IProcessService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IComponentAdapter))) + .returns(() => componentAdapter.object); condaService = TypeMoq.Mock.ofType<ICondaService>(); condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(conda)); bash = mock(Bash); + // eslint-disable-next-line @typescript-eslint/no-explicit-any processService.setup((x: any) => x.then).returns(() => undefined); procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); procServiceFactory @@ -93,15 +101,22 @@ suite('Terminal Environment Activation conda', () => { terminalHelper = new TerminalHelper( platformService.object, instance(mock(TerminalManager)), - condaService.object, + serviceContainer.object, instance(mock(InterpreterService)), configService.object, - new CondaActivationCommandProvider(condaService.object, platformService.object, configService.object), + new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object, + componentAdapter.object, + ), instance(bash), mock(CommandPromptAndPowerShell), + mock(Nushell), mock(PyEnvActivationCommandProvider), mock(PipEnvActivationCommandProvider), - [] + mock(PixiActivationCommandProvider), + [], ); }); teardown(() => { @@ -117,7 +132,7 @@ suite('Terminal Environment Activation conda', () => { const envName = 'EnvA'; const pythonPath = 'python3'; platformService.setup((p) => p.isWindows).returns(() => false); - condaService + componentAdapter .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) .returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); const expected = ['"path to conda" activate EnvA']; @@ -125,63 +140,38 @@ suite('Terminal Environment Activation conda', () => { const provider = new CondaActivationCommandProvider( condaService.object, platformService.object, - configService.object + configService.object, + componentAdapter.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 + componentAdapter .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) .returns(() => Promise.resolve({ name: envName, - path: path.dirname(pythonPath) - }) + 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 expected = [ + `source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgumentForPythonExt()} EnvA`, + ]; const provider = new CondaActivationCommandProvider( condaService.object, platformService.object, - configService.object + configService.object, + componentAdapter.object, ); const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); @@ -191,49 +181,89 @@ suite('Terminal Environment Activation conda', () => { const interpreterPath = path.join('path', 'to', 'interpreter'); const environmentName = 'Env'; const environmentNameHasSpaces = 'Env with spaces'; - const testsForActivationUsingInterpreterPath = [ + const testsForActivationUsingInterpreterPath: { + testName: string; + envName: string; + condaScope?: 'global' | 'local'; + condaInfo?: { + // eslint-disable-next-line camelcase + conda_shlvl?: number; + }; + expectedResult: string[]; + isWindows: boolean; + }[] = [ { 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 + 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 + expectedResult: ['source path/to/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 + 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 + expectedResult: ['source path/to/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 + 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 - } + expectedResult: ['source path/to/activate .'], + isWindows: false, + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, global conda, conda not sourced and with no spaces in env name', + envName: environmentName, + expectedResult: ['source path/to/activate Env'], + condaScope: 'global', + isWindows: false, + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, global conda, conda sourced and with no spaces in env name', + envName: environmentName, + expectedResult: ['conda activate Env'], + condaInfo: { + conda_shlvl: 1, + }, + condaScope: 'global', + isWindows: false, + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, local conda, conda sourced and with no spaces in env name', + envName: environmentName, + expectedResult: ['source path/to/activate Env'], + condaInfo: { + conda_shlvl: 1, + }, + condaScope: 'local', + isWindows: false, + }, ]; testsForActivationUsingInterpreterPath.forEach((testParams) => { @@ -241,25 +271,41 @@ suite('Terminal Environment Activation conda', () => { const pythonPath = 'python3'; platformService.setup((p) => p.isWindows).returns(() => testParams.isWindows); condaService.reset(); - condaService + componentAdapter .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) .returns(() => Promise.resolve({ name: testParams.envName, - path: path.dirname(pythonPath) - }) + 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)); + condaService + .setup((c) => c.getActivationScriptFromInterpreter(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + path: path.join(path.dirname(interpreterPath), 'activate').fileToCommandArgumentForPythonExt(), + type: testParams.condaScope ?? 'local', + }), + ); + + condaService.setup((c) => c.getCondaInfo()).returns(() => Promise.resolve(testParams.condaInfo)); + + // getActivationScriptFromInterpreter const provider = new CondaActivationCommandProvider( condaService.object, platformService.object, - configService.object + configService.object, + componentAdapter.object, + ); + + const activationCommands = await provider.getActivationCommands( + undefined, + testParams.isWindows ? TerminalShellType.commandPrompt : TerminalShellType.bash, ); - const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); expect(activationCommands).to.deep.equal(testParams.expectedResult, 'Incorrect Activation command'); }); @@ -271,21 +317,22 @@ suite('Terminal Environment Activation conda', () => { isLinux: boolean, pythonPath: string, shellType: TerminalShellType, - envName: string + 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)); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); - condaService + componentAdapter .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 + configService.object, + componentAdapter.object, ).getActivationCommands(undefined, shellType); let expectedActivationCommand: string[] | undefined; const expectEnvActivatePath = path.dirname(pythonPath); @@ -294,7 +341,7 @@ suite('Terminal Environment Activation conda', () => { case TerminalShellType.powershellCore: case TerminalShellType.fish: { if (envName !== '') { - expectedActivationCommand = [`conda activate ${envName.toCommandArgument()}`]; + expectedActivationCommand = [`conda activate ${envName.toCommandArgumentForPythonExt()}`]; } else { expectedActivationCommand = [`conda activate ${expectEnvActivatePath}`]; } @@ -303,8 +350,8 @@ suite('Terminal Environment Activation conda', () => { default: { if (envName !== '') { expectedActivationCommand = isWindows - ? [`activate ${envName.toCommandArgument()}`] - : [`source activate ${envName.toCommandArgument()}`]; + ? [`activate ${envName.toCommandArgumentForPythonExt()}`] + : [`source activate ${envName.toCommandArgumentForPythonExt()}`]; } else { expectedActivationCommand = isWindows ? [`activate ${expectEnvActivatePath}`] @@ -371,21 +418,21 @@ suite('Terminal Environment Activation conda', () => { isWindows: boolean, isOsx: boolean, isLinux: boolean, - pythonPath: string + 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)); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); - condaService + componentAdapter .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 + undefined, ); expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); } @@ -402,7 +449,7 @@ suite('Terminal Environment Activation conda', () => { 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'))) + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), ) .returns(() => Promise.resolve(true)); await expectCondaActivationCommand(false, false, true, pythonPath); @@ -412,7 +459,7 @@ suite('Terminal Environment Activation conda', () => { 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'))) + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), ) .returns(() => Promise.resolve(true)); await expectCondaActivationCommand(false, true, false, pythonPath); @@ -420,7 +467,7 @@ suite('Terminal Environment Activation conda', () => { 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)); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); const mockProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); @@ -438,7 +485,7 @@ suite('Terminal Environment Activation conda', () => { const activationCommands = await terminalHelper.getEnvironmentActivationCommands( TerminalShellType.bash, - undefined + undefined, ); expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); @@ -447,13 +494,13 @@ suite('Terminal Environment Activation conda', () => { isWindows: boolean, isOsx: boolean, isLinux: boolean, - pythonPath: string + 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)); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); when(bash.isShellSupported(anything())).thenReturn(true); @@ -462,7 +509,7 @@ suite('Terminal Environment Activation conda', () => { const expectedActivationCommand = ['mock command']; const activationCommands = await terminalHelper.getEnvironmentActivationCommands( TerminalShellType.bash, - undefined + undefined, ); expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); } @@ -479,7 +526,7 @@ suite('Terminal Environment Activation conda', () => { 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'))) + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), ) .returns(() => Promise.resolve(true)); await expectActivationCommandIfCondaDetectionFails(false, true, false, pythonPath); @@ -489,7 +536,7 @@ suite('Terminal Environment Activation conda', () => { 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'))) + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), ) .returns(() => Promise.resolve(true)); await expectActivationCommandIfCondaDetectionFails(false, false, true, pythonPath); @@ -498,7 +545,7 @@ suite('Terminal Environment Activation conda', () => { 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)); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); @@ -513,7 +560,7 @@ suite('Terminal Environment Activation conda', () => { const activationCommands = await terminalHelper.getEnvironmentActivationCommands( TerminalShellType.bash, - undefined + undefined, ); expect(activationCommands).to.equal(undefined, 'Incorrect Activation command'); }); @@ -537,7 +584,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershell + terminalKind: TerminalShellType.powershell, }, { testName: 'Activation uses full path with spaces on windows for powershell', @@ -545,7 +592,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.powershell + terminalKind: TerminalShellType.powershell, }, { testName: 'Activation uses full path on windows under powershell, environment name has spaces', @@ -553,7 +600,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'The Tester Environment', expectedResult: ['conda activate "The Tester Environment"'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershell + terminalKind: TerminalShellType.powershell, }, { testName: 'Activation uses full path on windows for powershell-core', @@ -561,7 +608,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershellCore + terminalKind: TerminalShellType.powershellCore, }, { testName: 'Activation uses full path with spaces on windows for powershell-core', @@ -569,7 +616,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.powershellCore + terminalKind: TerminalShellType.powershellCore, }, { testName: 'Activation uses full path on windows for powershell-core, environment name has spaces', @@ -577,7 +624,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'The Tester Environment', expectedResult: ['conda activate "The Tester Environment"'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershellCore + terminalKind: TerminalShellType.powershellCore, }, { testName: 'Activation uses full path on windows for cmd.exe', @@ -585,7 +632,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: [`${path.join(windowsTestPath, 'activate')} TesterEnv`], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.commandPrompt + terminalKind: TerminalShellType.commandPrompt, }, { testName: 'Activation uses full path with spaces on windows for cmd.exe', @@ -593,7 +640,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: [`"${path.join(windowsTestPathSpaces, 'activate')}" TesterEnv`], expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.commandPrompt + terminalKind: TerminalShellType.commandPrompt, }, { testName: 'Activation uses full path on windows for cmd.exe, environment name has spaces', @@ -601,29 +648,22 @@ suite('Terminal Environment Activation conda', () => { envName: 'The Tester Environment', expectedResult: [`${path.join(windowsTestPath, 'activate')} "The Tester Environment"`], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.commandPrompt - } + 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); + condaSrv.setup((c) => c.getCondaFile()).returns(async () => path.join(testParams.basePath, 'conda.exe')); const tstCmdProvider = new CondaActivationCommandProvider( condaSrv.object, platformService.object, - configService.object + configService.object, + componentAdapter.object, ); let result: string[] | undefined; @@ -631,7 +671,7 @@ suite('Terminal Environment Activation conda', () => { if (testParams.terminalKind === TerminalShellType.commandPrompt) { result = await tstCmdProvider.getWindowsCommands(testParams.envName); } else { - result = await tstCmdProvider.getPowershellCommands(testParams.envName); + result = await _getPowershellCommands(testParams.envName); } expect(result).to.deep.equal(testParams.expectedResult, 'Specific terminal command is incorrect.'); }); diff --git a/src/test/common/terminals/activation.nushell.unit.test.ts b/src/test/common/terminals/activation.nushell.unit.test.ts new file mode 100644 index 000000000000..bf748bc7c053 --- /dev/null +++ b/src/test/common/terminals/activation.nushell.unit.test.ts @@ -0,0 +1,72 @@ +// 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 { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; +import { TerminalShellType } from '../../../client/common/terminal/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'; + +const pythonPath = 'usr/bin/python'; + +suite('Terminal Environment Activation (nushell)', () => { + for (const scriptFileName of ['activate', 'activate.sh', 'activate.nu']) { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + 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); + + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); + }); + + for (const { name, value } of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const isNushell = value === TerminalShellType.nushell; + const isScriptFileSupported = isNushell && ['activate.nu'].includes(scriptFileName); + const expectedReturn = isScriptFileSupported ? 'activation command' : 'undefined'; + + // eslint-disable-next-line no-loop-func -- setup() takes care of shellType and fileSystem reinitialization + test(`Ensure nushell Activation command returns ${expectedReturn} (Shell: ${name})`, async () => { + const nu = new Nushell(serviceContainer.object); + + const supported = nu.isShellSupported(value); + if (isNushell) { + expect(supported).to.be.equal(true, `${name} shell not supported (it should be)`); + } else { + expect(supported).to.be.equal(false, `${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 nu.getActivationCommands(undefined, value); + + if (isScriptFileSupported) { + expect(command).to.be.deep.equal( + [`overlay use ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], + 'Invalid command', + ); + } else { + expect(command).to.be.equal(undefined, 'Command should be undefined'); + } + }); + } + }); + } +}); diff --git a/src/test/common/terminals/activation.unit.test.ts b/src/test/common/terminals/activation.unit.test.ts index b8ef12607395..d87d33ea03e6 100644 --- a/src/test/common/terminals/activation.unit.test.ts +++ b/src/test/common/terminals/activation.unit.test.ts @@ -3,6 +3,7 @@ 'use strict'; import { expect } from 'chai'; +import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Terminal, Uri } from 'vscode'; @@ -15,8 +16,8 @@ import { IDisposable } from '../../../client/common/types'; import { TerminalAutoActivation } from '../../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../../client/terminals/types'; import { noop } from '../../core'; +import * as extapi from '../../../client/envExt/api.internal'; -// tslint:disable-next-line: max-func-body-length suite('Terminal Auto Activation', () => { let activator: ITerminalActivator; let terminalManager: ITerminalManager; @@ -26,7 +27,8 @@ suite('Terminal Auto Activation', () => { let terminal: Terminal; setup(() => { - terminal = { + sinon.stub(extapi, 'shouldEnvExtHandleActivation').returns(false); + terminal = ({ dispose: noop, hide: noop, name: 'Python', @@ -34,8 +36,8 @@ suite('Terminal Auto Activation', () => { processId: Promise.resolve(0), sendText: noop, show: noop, - exitStatus: { code: 0 } - }; + exitStatus: { code: 0 }, + } as unknown) as Terminal; terminalManager = mock(TerminalManager); activator = mock(TerminalActivator); activeResourceService = mock(ActiveResourceService); @@ -44,9 +46,12 @@ suite('Terminal Auto Activation', () => { instance(terminalManager), [], instance(activator), - instance(activeResourceService) + instance(activeResourceService), ); }); + teardown(() => { + sinon.restore(); + }); test('New Terminals should be activated', async () => { type EventHandler = (e: Terminal) => void; @@ -69,7 +74,7 @@ suite('Terminal Auto Activation', () => { verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); }); test('New Terminals should not be activated if hidden from user', async () => { - terminal = { + terminal = ({ dispose: noop, hide: noop, name: 'Python', @@ -77,8 +82,38 @@ suite('Terminal Auto Activation', () => { processId: Promise.resolve(0), sendText: noop, show: noop, - exitStatus: { code: 0 } + exitStatus: { code: 0 }, + } as unknown) as Terminal; + 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 not be activated if auto activation is to be disabled', async () => { + terminal = ({ + dispose: noop, + hide: noop, + name: 'Python', + creationOptions: { hideFromUser: false }, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 }, + } as unknown) as Terminal; type EventHandler = (e: Terminal) => void; let handler: undefined | EventHandler; const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); @@ -91,6 +126,7 @@ suite('Terminal Auto Activation', () => { when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); terminalAutoActivation.register(); + terminalAutoActivation.disableAutoActivation(terminal); expect(handler).not.to.be.an('undefined', 'event handler not initialized'); diff --git a/src/test/common/terminals/activator/base.unit.test.ts b/src/test/common/terminals/activator/base.unit.test.ts index 79f1c121cdb4..fdfe9dcee579 100644 --- a/src/test/common/terminals/activator/base.unit.test.ts +++ b/src/test/common/terminals/activator/base.unit.test.ts @@ -10,7 +10,6 @@ import { BaseTerminalActivator } from '../../../../client/common/terminal/activa 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>; @@ -28,14 +27,14 @@ suite('Terminal Base Activator', () => { { commandCount: 1, preserveFocus: false }, { commandCount: 2, preserveFocus: false }, { commandCount: 1, preserveFocus: true }, - { 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()) + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => Promise.resolve(activationCommands)); const terminal = TypeMoq.Mock.ofType<Terminal>(); @@ -58,7 +57,7 @@ suite('Terminal Base Activator', () => { test(`Terminal is activated only once ${titleSuffix}`, async () => { helper .setup((h) => - h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => Promise.resolve(activationCommands)); const terminal = TypeMoq.Mock.ofType<Terminal>(); @@ -83,7 +82,7 @@ suite('Terminal Base Activator', () => { 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()) + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => Promise.resolve(activationCommands)); const terminal = TypeMoq.Mock.ofType<Terminal>(); @@ -102,7 +101,7 @@ suite('Terminal Base Activator', () => { 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 }) + activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }), ]); terminal.verifyAll(); diff --git a/src/test/common/terminals/activator/index.unit.test.ts b/src/test/common/terminals/activator/index.unit.test.ts index e9b1a1e0050e..34d1cf8f1bcd 100644 --- a/src/test/common/terminals/activator/index.unit.test.ts +++ b/src/test/common/terminals/activator/index.unit.test.ts @@ -4,26 +4,44 @@ 'use strict'; import { assert } from 'chai'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { Terminal } from 'vscode'; +import { Terminal, Uri } from 'vscode'; import { TerminalActivator } from '../../../../client/common/terminal/activator'; import { ITerminalActivationHandler, ITerminalActivator, - ITerminalHelper + ITerminalHelper, } from '../../../../client/common/terminal/types'; -import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../../client/common/types'; +import { + IConfigurationService, + IExperimentService, + IPythonSettings, + ITerminalSettings, +} from '../../../../client/common/types'; +import * as extapi from '../../../../client/envExt/api.internal'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import * as extensionsApi from '../../../../client/common/vscodeApis/extensionsApi'; -// 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>; + let experimentService: TypeMoq.IMock<IExperimentService>; + let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + baseActivator = TypeMoq.Mock.ofType<ITerminalActivator>(); terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); + experimentService = TypeMoq.Mock.ofType<IExperimentService>(); + experimentService.setup((e) => e.inExperimentSync(TypeMoq.It.isAny())).returns(() => false); handler1 = TypeMoq.Mock.ofType<ITerminalActivationHandler>(); handler2 = TypeMoq.Mock.ofType<ITerminalActivationHandler>(); const configService = TypeMoq.Mock.ofType<IConfigurationService>(); @@ -31,19 +49,28 @@ suite('Terminal Activator', () => { .setup((c) => c.getSettings(TypeMoq.It.isAny())) .returns(() => { return ({ - terminal: terminalSettings.object + 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); + })( + TypeMoq.Mock.ofType<ITerminalHelper>().object, + [handler1.object, handler2.object], + configService.object, + experimentService.object, + ); + }); + teardown(() => { + sinon.restore(); }); + async function testActivationAndHandlers( activationSuccessful: boolean, activateEnvironmentSetting: boolean, - hidden: boolean = false + hidden: boolean = false, ) { terminalSettings .setup((b) => b.activateEnvironment) @@ -59,8 +86,8 @@ suite('Terminal Activator', () => { TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), - TypeMoq.It.isValue(activationSuccessful) - ) + TypeMoq.It.isValue(activationSuccessful), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); @@ -70,8 +97,8 @@ suite('Terminal Activator', () => { TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), - TypeMoq.It.isValue(activationSuccessful) - ) + TypeMoq.It.isValue(activationSuccessful), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); @@ -79,10 +106,10 @@ suite('Terminal Activator', () => { const terminal = TypeMoq.Mock.ofType<Terminal>(); const activated = await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: activationSuccessful, - hideFromUser: hidden + hideFromUser: hidden, }); - assert.equal(activated, activationSuccessful); + assert.strictEqual(activated, activationSuccessful); baseActivator.verifyAll(); handler1.verifyAll(); handler2.verifyAll(); @@ -91,4 +118,92 @@ suite('Terminal Activator', () => { 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)); + + test('Terminal is not activated from Python extension when Env extension should handle activation', async () => { + shouldEnvExtHandleActivationStub.returns(true); + terminalSettings.setup((b) => b.activateEnvironment).returns(() => true); + baseActivator + .setup((b) => b.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.never()); + + const terminal = TypeMoq.Mock.ofType<Terminal>(); + const activated = await activator.activateEnvironmentInTerminal(terminal.object, { + preserveFocus: true, + }); + + assert.strictEqual(activated, false); + baseActivator.verifyAll(); + }); +}); + +suite('shouldEnvExtHandleActivation', () => { + let getExtensionStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + + setup(() => { + getExtensionStub = sinon.stub(extensionsApi, 'getExtension'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns(undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Returns false when envs extension is not installed', () => { + getExtensionStub.returns(undefined); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns true when envs extension is installed and setting is not explicitly set', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: undefined, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true); + }); + + test('Returns false when envs extension is installed but globalValue is false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: false, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns false when envs extension is installed but workspaceValue is false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: undefined, workspaceValue: false }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns true when envs extension is installed and setting is explicitly true', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: true, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true); + }); + + test('Returns false when a workspace folder has workspaceFolderValue set to false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + const folderUri = Uri.parse('file:///workspace/folder1'); + getWorkspaceFoldersStub.returns([{ uri: folderUri, name: 'folder1', index: 0 }]); + getConfigurationStub.callsFake((_section: string, scope?: Uri) => { + if (scope) { + return { + inspect: () => ({ workspaceFolderValue: false }), + }; + } + return { + inspect: () => ({ globalValue: undefined, workspaceValue: undefined }), + }; + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); }); diff --git a/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts b/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts index 0ca229c07dee..9bf1afbbad03 100644 --- a/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts +++ b/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts @@ -11,11 +11,10 @@ import { PowershellTerminalActivationFailedHandler } from '../../../../client/co import { ITerminalActivationHandler, ITerminalHelper, - TerminalShellType + 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>; @@ -27,7 +26,7 @@ suite('Terminal Activation Powershell Failed Handler', () => { isWindows: boolean, activatedSuccessfully: boolean, shellType: TerminalShellType, - cmdPromptHasActivationCommands: boolean + cmdPromptHasActivationCommands: boolean, ) { platform.setup((p) => p.isWindows).returns(() => isWindows); helper.setup((p) => p.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => shellType); @@ -36,8 +35,8 @@ suite('Terminal Activation Powershell Failed Handler', () => { .setup((h) => h.getEnvironmentActivationCommands( TypeMoq.It.isValue(TerminalShellType.commandPrompt), - TypeMoq.It.isAny() - ) + TypeMoq.It.isAny(), + ), ) .returns(() => Promise.resolve(cmdPromptCommands)); @@ -49,7 +48,7 @@ suite('Terminal Activation Powershell Failed Handler', () => { TypeMoq.Mock.ofType<Terminal>().object, undefined, false, - activatedSuccessfully + activatedSuccessfully, ); } @@ -78,7 +77,7 @@ suite('Terminal Activation Powershell Failed Handler', () => { psHandler = new PowershellTerminalActivationFailedHandler( helper.object, platform.object, - diagnosticService.object + diagnosticService.object, ); }); const isPs = @@ -94,15 +93,15 @@ suite('Terminal Activation Powershell Failed Handler', () => { isWindows, activatedSuccessfully, shell.value, - hasCommandPromptActivations + 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 index 2a240967d2b8..acea3f5f35a9 100644 --- a/src/test/common/terminals/commandPrompt.unit.test.ts +++ b/src/test/common/terminals/commandPrompt.unit.test.ts @@ -9,7 +9,7 @@ import * as TypeMoq from 'typemoq'; import { ConfigurationTarget } from 'vscode'; import { getCommandPromptLocation, - useCommandPromptAsDefaultShell + useCommandPromptAsDefaultShell, } from '../../../client/common/terminal/commandPrompt'; import { IConfigurationService, ICurrentProcess } from '../../../client/common/types'; @@ -60,8 +60,8 @@ suite('Terminal Command Prompt', () => { TypeMoq.It.isValue('integrated.shell.windows'), TypeMoq.It.isValue(cmdPromptPath), TypeMoq.It.isAny(), - TypeMoq.It.isValue(ConfigurationTarget.Global) - ) + TypeMoq.It.isValue(ConfigurationTarget.Global), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts index 950659895ee5..5d963b8aa2c2 100644 --- a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts +++ b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts @@ -9,16 +9,13 @@ 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 { IToolExecutionPath } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; -import { IInterpreterService, IPipEnvService } from '../../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { InterpreterService } from '../../../../client/interpreter/interpreterService'; -import { InterpreterType } from '../../../../client/pythonEnvironments/info'; - -// tslint:disable:no-any +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; suite('Terminals Activation - Pipenv', () => { [undefined, Uri.parse('x')].forEach((resource) => { @@ -26,23 +23,20 @@ suite('Terminals Activation - Pipenv', () => { let pipenvExecFile = 'pipenv'; let activationProvider: ITerminalActivationCommandProvider; let interpreterService: IInterpreterService; - let pipenvService: TypeMoq.IMock<IPipEnvService>; + let pipEnvExecution: TypeMoq.IMock<IToolExecutionPath>; let workspaceService: IWorkspaceService; - let fs: IFileSystem; setup(() => { interpreterService = mock(InterpreterService); - fs = mock(FileSystem); workspaceService = mock(WorkspaceService); interpreterService = mock(InterpreterService); - pipenvService = TypeMoq.Mock.ofType<IPipEnvService>(); + pipEnvExecution = TypeMoq.Mock.ofType<IToolExecutionPath>(); activationProvider = new PipEnvActivationCommandProvider( instance(interpreterService), - pipenvService.object, + pipEnvExecution.object, instance(workspaceService), - instance(fs) ); - pipenvService.setup((p) => p.executable).returns(() => pipenvExecFile); + pipEnvExecution.setup((p) => p.executable).returns(() => pipenvExecFile); }); test('No commands for no interpreter', async () => { @@ -51,28 +45,30 @@ suite('Terminals Activation - Pipenv', () => { for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { const cmd = await activationProvider.getActivationCommands(resource, shell.value); - assert.equal(cmd, undefined); + assert.strictEqual(cmd, undefined); } }); test('No commands for an interpreter that is not Pipenv', async () => { - const nonPipInterpreterTypes = getNamesAndValues<InterpreterType>(InterpreterType).filter( - (t) => t.value !== InterpreterType.Pipenv + const nonPipInterpreterTypes = getNamesAndValues<EnvironmentType>(EnvironmentType).filter( + (t) => t.value !== EnvironmentType.Pipenv, ); for (const interpreterType of nonPipInterpreterTypes) { when(interpreterService.getActiveInterpreter(resource)).thenResolve({ - type: interpreterType + type: interpreterType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { const cmd = await activationProvider.getActivationCommands(resource, shell.value); - assert.equal(cmd, undefined); + assert.strictEqual(cmd, undefined); } } }); test('pipenv shell is returned for pipenv interpeter', async () => { when(interpreterService.getActiveInterpreter(resource)).thenResolve({ - type: InterpreterType.Pipenv + envType: EnvironmentType.Pipenv, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { @@ -84,7 +80,8 @@ suite('Terminals Activation - Pipenv', () => { test('pipenv is properly escaped', async () => { pipenvExecFile = 'my pipenv'; when(interpreterService.getActiveInterpreter(resource)).thenResolve({ - type: InterpreterType.Pipenv + envType: EnvironmentType.Pipenv, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index e986708ac163..5a5e65a9c0f2 100644 --- a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -4,34 +4,30 @@ 'use strict'; import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as path from 'path'; +import * as sinon from 'sinon'; 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 + waitForCondition, } from '../../../common'; import { EXTENSION_ROOT_DIR_FOR_TESTS, TEST_TIMEOUT } from '../../../constants'; import { sleep } from '../../../core'; -import { initialize, initializeTest } from '../../../initialize'; +import { 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' + 'testExecInTerminal.py', ); let outputFile = ''; let outputFileCounter = 0; @@ -53,36 +49,36 @@ suite('Activation of Environments in Terminal', () => { const defaultShell = { Windows: '', Linux: '', - MacOS: '' + MacOS: '', }; let terminalSettings: any; let pythonSettings: any; - let experiments: IExperimentsManager; + const sandbox = sinon.createSandbox(); 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); + defaultShell.Windows = terminalSettings.inspect('integrated.defaultProfile.windows').globalValue; + defaultShell.Linux = terminalSettings.inspect('integrated.defaultProfile.linux').globalValue; + await terminalSettings.update('integrated.defaultProfile.linux', 'bash', vscode.ConfigurationTarget.Global); }); - setup(async () => { + setup(async function () { + this.skip(); // https://github.com/microsoft/vscode-python/issues/22264 await initializeTest(); outputFile = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', - `testExecInTerminal_${outputFileCounter}.log` + `testExecInTerminal_${outputFileCounter}.log`, ); outputFileCounter += 1; outputFilesCreated.push(outputFile); }); suiteTeardown(async function () { - // tslint:disable-next-line: no-invalid-this + sandbox.restore(); this.timeout(TEST_TIMEOUT * 2); await revertSettings(); @@ -99,20 +95,20 @@ suite('Activation of Environments in Terminal', () => { 'terminal.activateEnvironment', undefined, vscode.workspace.workspaceFolders![0].uri, - vscode.ConfigurationTarget.WorkspaceFolder + vscode.ConfigurationTarget.WorkspaceFolder, ); await terminalSettings.update( - 'integrated.shell.windows', + 'integrated.defaultProfile.windows', defaultShell.Windows, - vscode.ConfigurationTarget.Global + 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(); - } + await terminalSettings.update( + 'integrated.defaultProfile.linux', + defaultShell.Linux, + vscode.ConfigurationTarget.Global, + ); + await pythonSettings.update('condaPath', undefined, vscode.ConfigurationTarget.Global); + await restorePythonPathInWorkspaceRoot(); } /** @@ -128,11 +124,14 @@ suite('Activation of Environments in Terminal', () => { consoleInitWaitMs: number, pythonFile: string, logFile: string, - logFileCreationWaitMs: number + logFileCreationWaitMs: number, ): Promise<string> { const terminal = vscode.window.createTerminal(); await sleep(consoleInitWaitMs); - terminal.sendText(`python ${pythonFile} ${logFile}`, true); + terminal.sendText( + `python ${pythonFile.toCommandArgumentForPythonExt()} ${logFile.toCommandArgumentForPythonExt()}`, + true, + ); await waitForCondition(() => fs.pathExists(logFile), logFileCreationWaitMs, `${logFile} file not created.`); return fs.readFile(logFile, 'utf-8'); @@ -151,13 +150,9 @@ suite('Activation of Environments in Terminal', () => { 'terminal.activateEnvironment', true, vscode.workspace.workspaceFolders![0].uri, - vscode.ConfigurationTarget.WorkspaceFolder + vscode.ConfigurationTarget.WorkspaceFolder, ); - if (experiments.inExperiment(DeprecatePythonPath.experiment)) { - await setGlobalInterpreterPath(envPath); - } else { - await setPythonPathInWorkspaceRoot(envPath); - } + await setPythonPathInWorkspaceRoot(envPath); const content = await openTerminalAndAwaitCommandContent(waitTimeForActivation, file, outputFile, 5_000); expect(fileSystem.arePathsSame(content, envPath)).to.equal(true, 'Environment not activated'); } @@ -167,7 +162,7 @@ suite('Activation of Environments in Terminal', () => { 'terminal.activateEnvironment', false, vscode.workspace.workspaceFolders![0].uri, - vscode.ConfigurationTarget.WorkspaceFolder + vscode.ConfigurationTarget.WorkspaceFolder, ); const content = await openTerminalAndAwaitCommandContent(waitTimeForActivation, file, outputFile, 5_000); expect(fileSystem.arePathsSame(content, PYTHON_PATH)).to.equal(false, 'Environment not activated'); @@ -175,24 +170,27 @@ suite('Activation of Environments in Terminal', () => { 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 () => { + test('Should activate with pipenv', async function () { + if (process.env.CI_PYTHON_VERSION && process.env.CI_PYTHON_VERSION.startsWith('2.')) { + this.skip(); + } await testActivation(envPaths.pipenvPath); }); - test('Should activate with virtualenv', async () => { + test('Should activate with virtualenv', async function () { await testActivation(envPaths.virtualEnvPath); }); - test('Should activate with conda', async () => { + test('Should activate with conda', async function () { + // Powershell does not work with conda by default, hence use cmd. await terminalSettings.update( - 'integrated.shell.windows', - 'C:\\Windows\\System32\\cmd.exe', - vscode.ConfigurationTarget.Global + 'integrated.defaultProfile.windows', + 'Command Prompt', + vscode.ConfigurationTarget.Global, ); - await pythonSettings.update('condaPath', envPaths.condaExecPath, vscode.ConfigurationTarget.Workspace); + await pythonSettings.update('condaPath', envPaths.condaExecPath, vscode.ConfigurationTarget.Global); 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 index 1aeee9263f53..5ad2da8e793a 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -14,7 +14,6 @@ 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[] = []; @@ -55,33 +54,37 @@ suite('Terminal Service Factory', () => { }); test('Ensure same instance of terminal service is returned', () => { - const instance = factory.getTerminalService() as SynchronousTerminalService; + const instance = factory.getTerminalService({}) as SynchronousTerminalService; const sameInstance = - (factory.getTerminalService() as SynchronousTerminalService).terminalService === instance.terminalService; + (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 differentInstance = factory.getTerminalService({ resource: undefined, title: '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(); + const defaultInstance = factory.getTerminalService({}); expect(defaultInstance instanceof SynchronousTerminalService).to.equal( true, - 'Not an instance of Terminal service' + 'Not an instance of Terminal service', ); - const notSameAsDefaultInstance = factory.getTerminalService(undefined, 'New Title') === defaultInstance; + const notSameAsDefaultInstance = + factory.getTerminalService({ resource: undefined, title: '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 instance = factory.getTerminalService({ + resource: undefined, + title: 'New Title', + }) as SynchronousTerminalService; const sameInstance = - (factory.getTerminalService(undefined, 'New Title') as SynchronousTerminalService).terminalService === - instance.terminalService; + (factory.getTerminalService({ resource: undefined, title: '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 differentInstance = factory.getTerminalService({ resource: undefined, title: 'Another New Title' }); const notTheSameInstance = differentInstance === instance; expect(notTheSameInstance).not.to.equal(true, 'Instances are the same'); }); @@ -102,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure same terminal is returned when using resources from the same workspace', () => { + test('Ensure same terminal is returned when using different resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -123,9 +126,9 @@ suite('Terminal Service Factory', () => { .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 terminalForFile1A = factory.getTerminalService({ resource: file1A }) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService({ resource: file2A }) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService({ resource: fileB }) as SynchronousTerminalService; const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; expect(terminalsAreSameForWorkspaceA).to.equal(true, 'Instances are not the same for Workspace A'); @@ -134,7 +137,52 @@ suite('Terminal Service Factory', () => { terminalForFile1A.terminalService === terminalForFileB.terminalService; expect(terminalsForWorkspaceABAreDifferent).to.equal( false, - 'Instances should be different for different workspaces' + 'Instances should be different for different workspaces', + ); + }); + + test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different 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({ + resource: file1A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService({ + resource: file2A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService({ + resource: fileB, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; + expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are 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 index 2e0881f1cbab..0d130b573408 100644 --- a/src/test/common/terminals/helper.unit.test.ts +++ b/src/test/common/terminals/helper.unit.test.ts @@ -13,6 +13,7 @@ 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 { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; @@ -22,71 +23,103 @@ import { TerminalNameShellDetector } from '../../../client/common/terminal/shell import { IShellDetector, ITerminalActivationCommandProvider, - TerminalShellType + 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 { IComponentAdapter } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; -import { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; - -// tslint:disable:max-func-body-length no-any +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PixiActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pixiActivationProvider'; suite('Terminal Service helpers', () => { let helper: TerminalHelper; let terminalManager: ITerminalManager; let platformService: IPlatformService; - let condaService: ICondaService; + let condaService: IComponentAdapter; + let serviceContainer: IServiceContainer; let configurationService: IConfigurationService; let condaActivationProvider: ITerminalActivationCommandProvider; let bashActivationProvider: ITerminalActivationCommandProvider; let cmdActivationProvider: ITerminalActivationCommandProvider; + let nushellActivationProvider: ITerminalActivationCommandProvider; let pyenvActivationProvider: ITerminalActivationCommandProvider; let pipenvActivationProvider: ITerminalActivationCommandProvider; + let pixiActivationProvider: ITerminalActivationCommandProvider; let pythonSettings: PythonSettings; let shellDetectorIdentifyTerminalShell: sinon.SinonStub<[(Terminal | undefined)?], TerminalShellType>; let mockDetector: IShellDetector; - const pythonInterpreter: PythonInterpreter = { + const pythonInterpreter: PythonEnvironment = { path: '/foo/bar/python.exe', version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, }; function doSetup() { mockDetector = mock(TerminalNameShellDetector); terminalManager = mock(TerminalManager); platformService = mock(PlatformService); - condaService = mock(CondaService); + serviceContainer = mock<IServiceContainer>(); + condaService = mock<IComponentAdapter>(); + when(serviceContainer.get<IComponentAdapter>(IComponentAdapter)).thenReturn(instance(condaService)); configurationService = mock(ConfigurationService); condaActivationProvider = mock(CondaActivationCommandProvider); bashActivationProvider = mock(Bash); cmdActivationProvider = mock(CommandPromptAndPowerShell); + nushellActivationProvider = mock(Nushell); pyenvActivationProvider = mock(PyEnvActivationCommandProvider); pipenvActivationProvider = mock(PipEnvActivationCommandProvider); + pixiActivationProvider = mock(PixiActivationCommandProvider); pythonSettings = mock(PythonSettings); shellDetectorIdentifyTerminalShell = sinon.stub(ShellDetector.prototype, 'identifyTerminalShell'); helper = new TerminalHelper( instance(platformService), instance(terminalManager), - instance(condaService), + instance(serviceContainer), instance(mock(InterpreterService)), instance(configurationService), instance(condaActivationProvider), instance(bashActivationProvider), instance(cmdActivationProvider), + instance(nushellActivationProvider), instance(pyenvActivationProvider), instance(pipenvActivationProvider), - [instance(mockDetector)] + instance(pixiActivationProvider), + [instance(mockDetector)], ); } teardown(() => shellDetectorIdentifyTerminalShell.restore()); suite('Misc', () => { setup(doSetup); + test('Creating terminal should not automatically contain PYTHONSTARTUP', () => { + const theTitle = 'Hello'; + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + const term = helper.createTerminal(theTitle); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + const terminalOptions = args.env; + const safeTerminalOptions = terminalOptions || {}; + expect(safeTerminalOptions).to.not.have.property('PYTHONSTARTUP'); + }); + + test('Env should be undefined if not explicitly passed in ', () => { + 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.env).to.be.deep.equal(undefined); + }); test('Create terminal without a title', () => { const terminal = 'Terminal Created'; @@ -119,7 +152,7 @@ suite('Terminal Service helpers', () => { item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()} 1 2`; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgumentForPythonExt()} 1 2`; const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); @@ -163,7 +196,7 @@ suite('Terminal Service helpers', () => { item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()}`; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgumentForPythonExt()}`; const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); @@ -171,7 +204,7 @@ suite('Terminal Service helpers', () => { }); }); - function title(resource?: Uri, interpreter?: PythonInterpreter) { + function title(resource?: Uri, interpreter?: PythonEnvironment) { return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; } @@ -185,13 +218,13 @@ suite('Terminal Service helpers', () => { function ensureCondaIsSupported( isSupported: boolean, pythonPath: string, - condaActivationCommands: 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 + condaActivationCommands, ); } test('Activation command must return conda activation command if interpreter is conda', async () => { @@ -202,8 +235,8 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.equal(condaActivationCommands); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(condaActivationProvider.getActivationCommands(resource, anything())).once(); }); test('Activation command must return undefined if none of the proivders support the shell', async () => { @@ -212,18 +245,20 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(nushellActivationProvider.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 + resource, ); expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); @@ -237,16 +272,18 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(nushellActivationProvider.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(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); @@ -257,11 +294,16 @@ suite('Terminal Service helpers', () => { ensureCondaIsSupported(false, pythonPath, []); when(pipenvActivationProvider.getActivationCommands(resource, anything())).thenResolve( - expectCommand + expectCommand, ); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(true); - [bashActivationProvider, cmdActivationProvider, pyenvActivationProvider].forEach((provider) => { + [ + bashActivationProvider, + cmdActivationProvider, + nushellActivationProvider, + pyenvActivationProvider, + ].forEach((provider) => { when(provider.getActivationCommands(resource, anything())).thenResolve(['Something']); when(provider.isShellSupported(anything())).thenReturn(true); }); @@ -269,7 +311,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).never(); @@ -277,6 +319,7 @@ suite('Terminal Service helpers', () => { verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.getActivationCommands(resource, anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.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'; @@ -287,44 +330,51 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(nushellActivationProvider.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(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.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 () => { + test('Activation command must return command from Command Prompt if that is supported, and so is bash and nushell 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(nushellActivationProvider.getActivationCommands(resource, anything())).thenResolve([]); when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(nushellActivationProvider.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(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); - verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); + // It should not be called as command prompt already returns the activation commands and is higher priority. + verify(nushellActivationProvider.getActivationCommands(resource, anything())).never(); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); }); [undefined, pythonInterpreter].forEach((interpreter) => { test('Activation command for Shell must be empty for unknown os', async () => { @@ -334,7 +384,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationShellCommands( resource, item.value, - interpreter + interpreter, ); expect(cmd).to.equal(undefined, 'Command must be undefined'); } @@ -352,20 +402,27 @@ suite('Terminal Service helpers', () => { when(platformService.osType).thenReturn(osType); when(bashActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); when(cmdActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); + when(nushellActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); const cmd = await helper.getEnvironmentActivationShellCommands( resource, shellToExpect, - interpreter + 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); + if (interpreter) { + verify(pythonSettings.pythonPath).never(); + verify(condaService.isCondaEnvironment(pythonPath)).never(); + } else { + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); + } verify(bashActivationProvider.isShellSupported(shellToExpect)).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).never(); verify(pipenvActivationProvider.isShellSupported(anything())).never(); verify(cmdActivationProvider.isShellSupported(shellToExpect)).atLeast(1); + verify(nushellActivationProvider.isShellSupported(shellToExpect)).atLeast(1); }); }); }); diff --git a/src/test/common/terminals/pyenvActivationProvider.unit.test.ts b/src/test/common/terminals/pyenvActivationProvider.unit.test.ts index d6d87c42087d..404425791580 100644 --- a/src/test/common/terminals/pyenvActivationProvider.unit.test.ts +++ b/src/test/common/terminals/pyenvActivationProvider.unit.test.ts @@ -13,7 +13,7 @@ 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 { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Activation pyenv', () => { let serviceContainer: TypeMoq.IMock<IServiceContainer>; @@ -47,13 +47,13 @@ suite('Terminal Environment Activation pyenv', () => { }); test('Ensure no activation commands are returned if intrepreter is not pyenv', async () => { - const intepreterInfo: PythonInterpreter = { + const intepreterInfo: PythonEnvironment = { architecture: Architecture.Unknown, path: '', sysPrefix: '', version: new SemVer('1.1.1-alpha'), sysVersion: '', - type: InterpreterType.Unknown + envType: EnvironmentType.Unknown, }; interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) @@ -65,13 +65,13 @@ suite('Terminal Environment Activation pyenv', () => { }); test('Ensure no activation commands are returned if intrepreter envName is empty', async () => { - const intepreterInfo: PythonInterpreter = { + const intepreterInfo: PythonEnvironment = { architecture: Architecture.Unknown, path: '', sysPrefix: '', version: new SemVer('1.1.1-alpha'), sysVersion: '', - type: InterpreterType.Pyenv + envType: EnvironmentType.Pyenv, }; interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) @@ -83,14 +83,14 @@ suite('Terminal Environment Activation pyenv', () => { }); test('Ensure activation command is returned', async () => { - const intepreterInfo: PythonInterpreter = { + const intepreterInfo: PythonEnvironment = { architecture: Architecture.Unknown, path: '', sysPrefix: '', version: new SemVer('1.1.1-alpha'), sysVersion: '', - type: InterpreterType.Pyenv, - envName: 'my env name' + envType: EnvironmentType.Pyenv, + envName: 'my env name', }; interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) @@ -100,7 +100,7 @@ suite('Terminal Environment Activation pyenv', () => { const activationCommands = await activationProvider.getActivationCommands(undefined, TerminalShellType.bash); expect(activationCommands).to.deep.equal( [`pyenv shell "${intepreterInfo.envName}"`], - 'Invalid Activation command' + 'Invalid Activation command', ); }); }); diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 901c0eb63697..3a6d54c9390b 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -2,16 +2,40 @@ // Licensed under the MIT License. import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { Disposable, Terminal as VSCodeTerminal, WorkspaceConfiguration } from 'vscode'; -import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { + Disposable, + EventEmitter, + TerminalShellExecution, + TerminalShellExecutionEndEvent, + TerminalShellIntegration, + Uri, + Terminal as VSCodeTerminal, + WorkspaceConfiguration, + TerminalDataWriteEvent, +} from 'vscode'; +import { IApplicationShell, ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalService } from '../../../client/common/terminal/service'; -import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { + ITerminalActivator, + ITerminalHelper, + TerminalCreationOptions, + TerminalShellType, +} from '../../../client/common/terminal/types'; import { IDisposableRegistry } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; +import { ITerminalAutoActivation } from '../../../client/terminals/types'; +import { createPythonInterpreter } from '../../utils/interpreters'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as platform from '../../../client/common/utils/platform'; +import * as extapi from '../../../client/envExt/api.internal'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -// tslint:disable-next-line:max-func-body-length suite('Terminal Service', () => { let service: TerminalService; let terminal: TypeMoq.IMock<VSCodeTerminal>; @@ -22,29 +46,111 @@ suite('Terminal Service', () => { let workspaceService: TypeMoq.IMock<IWorkspaceService>; let disposables: Disposable[] = []; let mockServiceContainer: TypeMoq.IMock<IServiceContainer>; + let terminalAutoActivator: TypeMoq.IMock<ITerminalAutoActivation>; + let terminalShellIntegration: TypeMoq.IMock<TerminalShellIntegration>; + let onDidEndTerminalShellExecutionEmitter: EventEmitter<TerminalShellExecutionEndEvent>; + let event: TerminalShellExecutionEndEvent; + let getConfigurationStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let editorConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let isWindowsStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let options: TypeMoq.IMock<TerminalCreationOptions>; + let applicationShell: TypeMoq.IMock<IApplicationShell>; + let onDidWriteTerminalDataEmitter: EventEmitter<TerminalDataWriteEvent>; + let onDidChangeTerminalStateEmitter: EventEmitter<VSCodeTerminal>; + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + terminal = TypeMoq.Mock.ofType<VSCodeTerminal>(); + terminalShellIntegration = TypeMoq.Mock.ofType<TerminalShellIntegration>(); + terminal.setup((t) => t.shellIntegration).returns(() => terminalShellIntegration.object); + + onDidEndTerminalShellExecutionEmitter = new EventEmitter<TerminalShellExecutionEndEvent>(); terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); + const execution: TerminalShellExecution = { + commandLine: { + value: 'dummy text', + isTrusted: true, + confidence: 2, + }, + cwd: undefined, + read: function (): AsyncIterable<string> { + throw new Error('Function not implemented.'); + }, + }; + + event = { + execution, + exitCode: 0, + terminal: terminal.object, + shellIntegration: terminalShellIntegration.object, + }; + + terminalShellIntegration.setup((t) => t.executeCommand(TypeMoq.It.isAny())).returns(() => execution); + + terminalManager + .setup((t) => t.onDidEndTerminalShellExecution) + .returns(() => { + setTimeout(() => onDidEndTerminalShellExecutionEmitter.fire(event), 100); + return onDidEndTerminalShellExecutionEmitter.event; + }); platformService = TypeMoq.Mock.ofType<IPlatformService>(); workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); terminalHelper = TypeMoq.Mock.ofType<ITerminalHelper>(); terminalActivator = TypeMoq.Mock.ofType<ITerminalActivator>(); + terminalAutoActivator = TypeMoq.Mock.ofType<ITerminalAutoActivation>(); disposables = []; mockServiceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + options = TypeMoq.Mock.ofType<TerminalCreationOptions>(); + options.setup((o) => o.resource).returns(() => Uri.parse('a')); + 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); + mockServiceContainer.setup((c) => c.get(ITerminalAutoActivation)).returns(() => terminalAutoActivator.object); + mockServiceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); + + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); + onDidWriteTerminalDataEmitter = new EventEmitter<TerminalDataWriteEvent>(); + applicationShell.setup((a) => a.onDidWriteTerminalData).returns(() => onDidWriteTerminalDataEmitter.event); + mockServiceContainer.setup((c) => c.get(IApplicationShell)).returns(() => applicationShell.object); + + onDidChangeTerminalStateEmitter = new EventEmitter<VSCodeTerminal>(); + terminalManager + .setup((t) => t.onDidChangeTerminalState(TypeMoq.It.isAny())) + .returns((handler) => onDidChangeTerminalStateEmitter.event(handler)); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + isWindowsStub = sinon.stub(platform, 'isWindows'); + pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + editorConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + getConfigurationStub.callsFake((section: string) => { + if (section === 'python') { + return pythonConfig.object; + } + return editorConfig.object; + }); }); teardown(() => { if (service) { - // tslint:disable-next-line:no-any service.dispose(); } disposables.filter((item) => !!item).forEach((item) => item.dispose()); + sinon.restore(); + interpreterService.reset(); }); test('Ensure terminal is disposed', async () => { @@ -54,6 +160,7 @@ suite('Terminal Service', () => { const os: string = 'windows'; service = new TerminalService(mockServiceContainer.object); const shellPath = 'powershell.exe'; + // TODO: switch over legacy Terminal code to use workspace getConfiguration from workspaceApis instead of directly from vscode.workspace workspaceService .setup((w) => w.getConfiguration(TypeMoq.It.isValue('terminal.integrated.shell'))) .returns(() => { @@ -61,6 +168,7 @@ suite('Terminal Service', () => { workspaceConfig.setup((c) => c.get(os)).returns(() => shellPath); return workspaceConfig.object; }); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); platformService.setup((p) => p.isWindows).returns(() => os === 'windows'); platformService.setup((p) => p.isLinux).returns(() => os === 'linux'); @@ -70,15 +178,22 @@ suite('Terminal Service', () => { .setup((h) => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => 'dummy text'); + terminalManager + .setup((t) => t.onDidEndTerminalShellExecution) + .returns(() => { + setTimeout(() => onDidEndTerminalShellExecutionEmitter.fire(event), 100); + return onDidEndTerminalShellExecutionEmitter.event; + }); // 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)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); service.dispose(); terminal.verify((t) => t.dispose(), TypeMoq.Times.exactly(1)); }); test('Ensure command is sent to terminal and it is shown', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); terminalHelper .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)); @@ -94,10 +209,10 @@ suite('Terminal Service', () => { await service.sendCommand(commandToSend, args); - terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); terminal.verify( (t) => t.sendText(TypeMoq.It.isValue(commandToExpect), TypeMoq.It.isValue(true)), - TypeMoq.Times.exactly(1) + TypeMoq.Times.never(), ); }); @@ -116,6 +231,156 @@ suite('Terminal Service', () => { terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); + test('Ensure sendText is used when Python shell integration is disabled', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + 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.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure sendText is called when terminal.shellIntegration enabled but Python shell integration disabled', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + 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.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python < 3.13', async () => { + isWindowsStub.returns(false); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + 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.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python >= 3.13', async () => { + interpreterService.reset(); + + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ path: 'yo', version: { major: 3, minor: 13, patch: 0 } } as PythonEnvironment), + ); + + isWindowsStub.returns(false); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + service = new TerminalService(mockServiceContainer.object, options.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.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); + }); + + test('Ensure sendText IS called even when Python shell integration and terminal shell integration are both enabled - Window', async () => { + isWindowsStub.returns(true); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + 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.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure REPL ready when onDidChangeTerminalState fires with python shell', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + 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); + + terminal.setup((t) => t.state).returns(() => ({ isInteractedWith: true, shell: 'python' })); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidChangeTerminalStateEmitter.fire(terminal.object); + await executePromise; + + 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())) @@ -155,6 +420,37 @@ suite('Terminal Service', () => { terminal.verify((t) => t.show(TypeMoq.It.isValue(false)), TypeMoq.Times.exactly(2)); }); + test('Ensure PYTHONSTARTUP is injected', 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()); + const envVarScript = path.join(EXTENSION_ROOT_DIR, 'python_files', 'pythonrc.py'); + terminalManager + .setup((t) => + t.createTerminal({ + name: TypeMoq.It.isAny(), + env: TypeMoq.It.isObjectWith({ PYTHONSTARTUP: envVarScript }), + hideFromUser: 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 after creation', async () => { service = new TerminalService(mockServiceContainer.object); terminalActivator @@ -208,7 +504,7 @@ suite('Terminal Service', () => { .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); @@ -234,7 +530,7 @@ suite('Terminal Service', () => { .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); @@ -250,4 +546,22 @@ suite('Terminal Service', () => { eventHandler!.bind(service)(terminal.object); expect(eventFired).to.be.equal(true, 'Event not fired'); }); + test('Ensure to disable auto activation and right interpreter is activated', async () => { + const interpreter = createPythonInterpreter({ path: 'abc' }); + service = new TerminalService(mockServiceContainer.object, { interpreter }); + + 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'); + + // Ensure we disable auto activation of the terminal. + terminalAutoActivator.verify((t) => t.disableAutoActivation(terminal.object), TypeMoq.Times.once()); + // Ensure the terminal is activated with the interpreter info. + terminalActivator.verify( + (t) => t.activateEnvironmentInTerminal(terminal.object, TypeMoq.It.isObjectWith({ interpreter })), + TypeMoq.Times.once(), + ); + }); }); diff --git a/src/test/common/terminals/serviceRegistry.unit.test.ts b/src/test/common/terminals/serviceRegistry.unit.test.ts index 8bc7e1670016..c6c03fec05a1 100644 --- a/src/test/common/terminals/serviceRegistry.unit.test.ts +++ b/src/test/common/terminals/serviceRegistry.unit.test.ts @@ -4,10 +4,9 @@ '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 { 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'; @@ -18,7 +17,7 @@ import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, - ITerminalAutoActivation + ITerminalAutoActivation, } from '../../../client/terminals/types'; suite('Common Terminal Service Registry', () => { @@ -38,27 +37,20 @@ suite('Common Terminal Service Registry', () => { serviceManager.addSingleton<ICodeExecutionService>( ICodeExecutionService, DjangoShellCodeExecutionProvider, - 'djangoShell' - ) + 'djangoShell', + ), ).once(); verify( serviceManager.addSingleton<ICodeExecutionService>( ICodeExecutionService, TerminalCodeExecutionProvider, - 'standard' - ) + 'standard', + ), ).once(); verify(serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, ReplProvider, 'repl')).once(); verify( - serviceManager.addSingleton<ITerminalAutoActivation>(ITerminalAutoActivation, TerminalAutoActivation) - ).once(); - - verify( - serviceManager.addSingleton<IExtensionSingleActivationService>( - IExtensionSingleActivationService, - ExtensionActivationForTerminalActivation - ) + serviceManager.addSingleton<ITerminalAutoActivation>(ITerminalAutoActivation, TerminalAutoActivation), ).once(); }); }); diff --git a/src/test/common/terminals/shellDetector.unit.test.ts b/src/test/common/terminals/shellDetector.unit.test.ts index 8afa2b18ba4a..c09560a3ea37 100644 --- a/src/test/common/terminals/shellDetector.unit.test.ts +++ b/src/test/common/terminals/shellDetector.unit.test.ts @@ -20,15 +20,13 @@ 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 + [OSType.Unknown]: TerminalShellType.other, }; const sandbox = sinon.createSandbox(); setup(() => (platformService = mock(PlatformService))); @@ -36,7 +34,7 @@ suite('Shell Detector', () => { getNamesAndValues<OSType>(OSType).forEach((os) => { const testSuffix = `(OS ${os.name})`; - test('Test identification of Terminal Shells in order of priority', async () => { + test(`Test identification of Terminal Shells in order of priority ${testSuffix}`, async () => { const callOrder: string[] = []; const nameDetectorIdentify = sandbox.stub(TerminalNameShellDetector.prototype, 'identify'); nameDetectorIdentify.callsFake(() => { @@ -65,7 +63,7 @@ suite('Shell Detector', () => { const userEnvDetector = new UserEnvironmentShellDetector(mock(MockProcess), instance(platformService)); const settingsDetector = new SettingsShellDetector( instance(mock(WorkspaceService)), - instance(platformService) + instance(platformService), ); const detectors = [settingsDetector, userEnvDetector, nameDetector, vscEnvDetector]; const shellDetector = new ShellDetector(instance(platformService), detectors); @@ -131,7 +129,7 @@ suite('Shell Detector', () => { const shellDetector = new ShellDetector(instance(platformService), [ instance(detector1), instance(detector2), - instance(detector3) + instance(detector3), ]); const shell = shellDetector.identifyTerminalShell(); @@ -160,7 +158,7 @@ suite('Shell Detector', () => { instance(detector1), instance(detector2), instance(detector3), - instance(detector4) + instance(detector4), ]); const shell = shellDetector.identifyTerminalShell(); @@ -190,7 +188,7 @@ suite('Shell Detector', () => { instance(detector1), instance(detector2), instance(detector3), - instance(detector4) + instance(detector4), ]); const shell = shellDetector.identifyTerminalShell(); diff --git a/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts index 7abd0c28d74e..e58e455ea7eb 100644 --- a/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts +++ b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts @@ -6,6 +6,7 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { instance, mock, when } from 'ts-mockito'; +import { Terminal } from 'vscode'; import { ApplicationEnvironment } from '../../../../client/common/application/applicationEnvironment'; import { WorkspaceService } from '../../../../client/common/application/workspace'; import { PlatformService } from '../../../../client/common/platform/platformService'; @@ -19,8 +20,6 @@ import { ShellIdentificationTelemetry, TerminalShellType } from '../../../../cli 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; @@ -34,10 +33,15 @@ suite('Shell Detectors', () => { 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('c:\\cygwin\\bin\\bash.exe', TerminalShellType.bash); + shellPathsAndIdentification.set('c:\\cygwin64\\bin\\bash.exe', TerminalShellType.bash); shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.zsh); + shellPathsAndIdentification.set('c:\\cygwin\\bin\\zsh.exe', TerminalShellType.zsh); + shellPathsAndIdentification.set('c:\\cygwin64\\bin\\zsh.exe', 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('C:\\Program Files\\nu\\bin\\nu.EXE', TerminalShellType.nushell); 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); @@ -48,15 +52,16 @@ suite('Shell Detectors', () => { 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 - }; + let telemetryProperties: ShellIdentificationTelemetry; setup(() => { + telemetryProperties = { + failed: true, + shellIdentificationSource: 'default', + terminalProvided: false, + hasCustomShell: undefined, + hasShellInEnv: undefined, + }; platformService = mock(PlatformService); workspaceService = mock(WorkspaceService); currentProcess = mock(CurrentProcess); @@ -67,7 +72,7 @@ suite('Shell Detectors', () => { 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 + 1, ); }); test('Test identification of Terminal Shells (base class method)', async () => { @@ -75,7 +80,7 @@ suite('Shell Detectors', () => { shellPathsAndIdentification.forEach((shellType, shellPath) => { expect(shellDetector.identifyShellFromShellPath(shellPath)).to.equal( shellType, - `Incorrect Shell Type for path '${shellPath}'` + `Incorrect Shell Type for path '${shellPath}'`, ); }); }); @@ -84,30 +89,42 @@ suite('Shell Detectors', () => { shellPathsAndIdentification.forEach((shellType, shellPath) => { expect(shellDetector.identify(telemetryProperties, { name: shellPath } as any)).to.equal( shellType, - `Incorrect Shell Type for name '${shellPath}'` + `Incorrect Shell Type for name '${shellPath}'`, ); }); expect(shellDetector.identify(telemetryProperties, undefined)).to.equal( undefined, - 'Should be undefined when there is no temrinal' + 'Should be undefined when there is no temrinal', ); }); - test('Identify shell based on VSC Environment', async () => { + test('Identify shell based on custom VSC shell path', async () => { + const shellDetector = new VSCEnvironmentShellDetector(instance(appEnv)); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + when(appEnv.shell).thenReturn('defaultshellPath'); + expect( + shellDetector.identify(telemetryProperties, ({ + creationOptions: { shellPath }, + } as unknown) as Terminal), + ).to.equal(shellType, `Incorrect Shell Type from identifyShellByTerminalName, for path '${shellPath}'`); + }); + }); + test('Identify shell based on VSC API', 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}'` + `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' + 'Should be undefined when vscode.env.shell is undefined', ); + expect(telemetryProperties.failed).to.equal(false); }); test('Identify shell based on VSC Settings', async () => { const shellDetector = new SettingsShellDetector(instance(workspaceService), instance(platformService)); @@ -116,7 +133,7 @@ suite('Shell Detectors', () => { shellDetector.getTerminalShellPath = () => shellPath; expect(shellDetector.identify(telemetryProperties, {} as any)).to.equal( shellType, - `Incorrect Shell Type for path '${shellPath}'` + `Incorrect Shell Type for path '${shellPath}'`, ); }); }); @@ -146,7 +163,7 @@ suite('Shell Detectors', () => { shellDetector.getDefaultPlatformShell = () => shellPath; expect(shellDetector.identify(telemetryProperties, {} as any)).to.equal( shellType, - `Incorrect Shell Type for path '${shellPath}'` + `Incorrect Shell Type for path '${shellPath}'`, ); }); }); diff --git a/src/test/common/terminals/synchronousTerminalService.unit.test.ts b/src/test/common/terminals/synchronousTerminalService.unit.test.ts index 082eeb1953ca..4b6e77ec8095 100644 --- a/src/test/common/terminals/synchronousTerminalService.unit.test.ts +++ b/src/test/common/terminals/synchronousTerminalService.unit.test.ts @@ -16,7 +16,6 @@ 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; @@ -28,7 +27,7 @@ suite('Terminal Service (synchronous)', () => { terminalService = mock(TerminalService); service = new SynchronousTerminalService(instance(fs), instance(interpreterService), instance(terminalService)); }); - suite('Show, sendText and dispose should invoke corressponding methods in wrapped TerminalService', () => { + suite('Show, sendText and dispose should invoke corresponding methods in wrapped TerminalService', () => { test('Show should invoke show in terminal', async () => { when(terminalService.show(anything())).thenResolve(); await service.show(); @@ -67,8 +66,7 @@ suite('Terminal Service (synchronous)', () => { }); }); suite('sendCommand', () => { - const isolated = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py'); - const shellExecFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'shell_exec.py'); + const shellExecFile = path.join(EXTENSION_ROOT_DIR, 'python_files', 'shell_exec.py'); test('run sendCommand in terminalService if there is no cancellation token', async () => { when(terminalService.sendCommand('cmd', deepEqual(['1', '2']))).thenResolve(); @@ -107,8 +105,8 @@ suite('Terminal Service (synchronous)', () => { verify( terminalService.sendCommand( 'python', - deepEqual([isolated, shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgument()]) - ) + deepEqual([shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgumentForPythonExt()]), + ), ).once(); }).timeout(1_000); test('run sendCommand in terminalService should complete when command completes', async () => { @@ -143,8 +141,8 @@ suite('Terminal Service (synchronous)', () => { verify( terminalService.sendCommand( 'python', - deepEqual([isolated, shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgument()]) - ) + deepEqual([shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgumentForPythonExt()]), + ), ).once(); }).timeout(2_000); }); diff --git a/src/test/common/utils/async.unit.test.ts b/src/test/common/utils/async.unit.test.ts index e3e03223fca0..6b6d41d552c3 100644 --- a/src/test/common/utils/async.unit.test.ts +++ b/src/test/common/utils/async.unit.test.ts @@ -4,7 +4,7 @@ 'use strict'; import * as assert from 'assert'; -import { createDeferred } from '../../../client/common/utils/async'; +import { chain, createDeferred, flattenIterator } from '../../../client/common/utils/async'; suite('Deferred', () => { test('Resolve', (done) => { @@ -12,21 +12,21 @@ suite('Deferred', () => { const def = createDeferred<number>(); def.promise .then((value) => { - assert.equal(value, valueToSent); - assert.equal(def.resolved, true, 'resolved property value is not `true`'); + assert.strictEqual(value, valueToSent); + assert.strictEqual(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'); + assert.strictEqual(def.resolved, false, 'Promise is resolved even when it should not be'); + assert.strictEqual(def.rejected, false, 'Promise is rejected even when it should not be'); + assert.strictEqual(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'); + assert.strictEqual(def.resolved, true, 'Promise is not resolved even when it should not be'); + assert.strictEqual(def.rejected, false, 'Promise is rejected even when it should not be'); + assert.strictEqual(def.completed, true, 'Promise is not completed even when it should not be'); }); test('Reject', (done) => { const errorToSend = new Error('Something'); @@ -37,19 +37,310 @@ suite('Deferred', () => { done(); }) .catch((reason) => { - assert.equal(reason, errorToSend, 'Error received is not the same'); + assert.strictEqual(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'); + assert.strictEqual(def.resolved, false, 'Promise is resolved even when it should not be'); + assert.strictEqual(def.rejected, false, 'Promise is rejected even when it should not be'); + assert.strictEqual(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'); + assert.strictEqual(def.resolved, false, 'Promise is resolved even when it should not be'); + assert.strictEqual(def.rejected, true, 'Promise is not rejected even when it should not be'); + assert.strictEqual(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>(); + // eslint-disable-next-line require-yield + 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(); + })(); + // eslint-disable-next-line require-yield + 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('A failed iterator does not block the others, if throwing before yielding.', 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', 'c']; + const it1 = (async function* () { + yield 'a'; + deferred12.resolve(); + })(); + const failure = new Error('uh-oh!'); + const it2 = (async function* () { + await deferred12.promise; + deferred23.resolve(); + throw failure; + yield 'b'; + })(); + 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 index b6859a82342d..01a11f4b4585 100644 --- a/src/test/common/utils/cacheUtils.unit.test.ts +++ b/src/test/common/utils/cacheUtils.unit.test.ts @@ -3,51 +3,10 @@ 'use strict'; -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as sinon from 'sinon'; -import { Uri } from 'vscode'; -import { clearCache, InMemoryCache, InMemoryInterpreterSpecificCache } from '../../../client/common/utils/cacheUtils'; +import { InMemoryCache } 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; @@ -59,249 +18,39 @@ suite('Common Utils - CacheUtils', () => { const cache = new InMemoryCache(5_000); cache.data = 'Hello World'; - assert.equal(cache.data, 'Hello World'); + assert.strictEqual(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.strictEqual(cache.data, 'Hello World'); assert.isOk(cache.hasData); cache.data = 'Bye'; - assert.equal(cache.data, 'Bye'); + assert.strictEqual(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.strictEqual(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.strictEqual(cache.data, 'Hello World'); assert.isTrue(cache.hasData); // Should expire after 5s (previous 4999ms + 1ms). clock.tick(1); - assert.equal(cache.data, undefined); + assert.strictEqual(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 index 8ff3c81c0ca6..b1e86c4e2013 100644 --- a/src/test/common/utils/decorators.unit.test.ts +++ b/src/test/common/utils/decorators.unit.test.ts @@ -8,14 +8,13 @@ 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); +use(chaiPromise.default); -// 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; @@ -73,8 +72,6 @@ suite('Common Utils - Decorators', function () { * 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(); @@ -92,9 +89,6 @@ suite('Common Utils - Decorators', function () { * 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; @@ -103,10 +97,10 @@ suite('Common Utils - Decorators', function () { } expect(Math.abs(difference)).to.be.lessThan( expectedDelay * 0.05, - `Actual delay ${actualDelay}, expected delay ${expectedDelay}, not within 5% of accuracy` + `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[]; @@ -138,7 +132,7 @@ suite('Common Utils - Decorators', function () { } 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 { @@ -158,7 +152,7 @@ suite('Common Utils - Decorators', function () { }); 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> { @@ -180,7 +174,7 @@ suite('Common Utils - Decorators', function () { }); 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> { @@ -200,7 +194,7 @@ suite('Common Utils - Decorators', function () { }); 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> { @@ -223,7 +217,7 @@ suite('Common Utils - Decorators', function () { }); 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> { @@ -248,7 +242,7 @@ suite('Common Utils - Decorators', function () { }); 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> { @@ -268,7 +262,7 @@ suite('Common Utils - Decorators', function () { }); 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> { @@ -293,7 +287,7 @@ suite('Common Utils - Decorators', function () { }); 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 { @@ -315,7 +309,7 @@ suite('Common Utils - Decorators', function () { }); 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 { diff --git a/src/test/common/utils/exec.unit.test.ts b/src/test/common/utils/exec.unit.test.ts new file mode 100644 index 000000000000..aebfbe7a417d --- /dev/null +++ b/src/test/common/utils/exec.unit.test.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { OSType } from '../../common'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; + +suite('Utils for exec - getSearchPathEnvVarNames function', () => { + const testsData = [ + { os: 'Unknown', expected: ['PATH'] }, + { os: 'Windows', expected: ['Path', 'PATH'] }, + { os: 'OSX', expected: ['PATH'] }, + { os: 'Linux', expected: ['PATH'] }, + ]; + + testsData.forEach((testData) => { + test(`getSearchPathEnvVarNames when os is ${testData.os}`, () => { + const pathVariables = getSearchPathEnvVarNames(testData.os as OSType); + + expect(pathVariables).to.deep.equal(testData.expected); + }); + }); +}); diff --git a/src/test/common/utils/filesystem.unit.test.ts b/src/test/common/utils/filesystem.unit.test.ts new file mode 100644 index 000000000000..a1c53edc73e9 --- /dev/null +++ b/src/test/common/utils/filesystem.unit.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { convertFileType } from '../../../client/common/utils/filesystem'; + +class KnowsFileTypeDummyImpl { + private _isFile: boolean; + + private _isDirectory: boolean; + + private _isSymbolicLink: boolean; + + constructor(isFile = false, isDirectory = false, isSymbolicLink = false) { + this._isFile = isFile; + this._isDirectory = isDirectory; + this._isSymbolicLink = isSymbolicLink; + } + + public isFile() { + return this._isFile; + } + + public isDirectory() { + return this._isDirectory; + } + + public isSymbolicLink() { + return this._isSymbolicLink; + } +} + +suite('Utils for filesystem - convertFileType function', () => { + const testsData = [ + { info: new KnowsFileTypeDummyImpl(true, false, false), kind: 'File', expected: 1 }, + { info: new KnowsFileTypeDummyImpl(false, true, false), kind: 'Directory', expected: 2 }, + { info: new KnowsFileTypeDummyImpl(false, false, true), kind: 'Symbolic Link', expected: 64 }, + { info: new KnowsFileTypeDummyImpl(false, false, false), kind: 'Unknown', expected: 0 }, + ]; + + testsData.forEach((testData) => { + test(`convertFileType when info is a ${testData.kind}`, () => { + const fileType = convertFileType(testData.info); + + expect(fileType).equals(testData.expected); + }); + }); +}); diff --git a/src/test/common/utils/localize.functional.test.ts b/src/test/common/utils/localize.functional.test.ts deleted file mode 100644 index 8ccd3cf58926..000000000000 --- a/src/test/common/utils/localize.functional.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -// 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/platform.unit.test.ts b/src/test/common/utils/platform.unit.test.ts new file mode 100644 index 000000000000..b27708978fc1 --- /dev/null +++ b/src/test/common/utils/platform.unit.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { OSType, getOSType } from '../../../client/common/utils/platform'; + +suite('Utils for platform - getOSType function', () => { + const testsData = [ + { platform: 'linux', expected: OSType.Linux }, + { platform: 'darwin', expected: OSType.OSX }, + { platform: 'anunknownplatform', expected: OSType.Unknown }, + { platform: 'windows', expected: OSType.Windows }, + ]; + + testsData.forEach((testData) => { + test(`getOSType when platform is ${testData.platform}`, () => { + const osType = getOSType(testData.platform); + expect(osType).equal(testData.expected); + }); + }); +}); diff --git a/src/test/common/utils/regexp.unit.test.ts b/src/test/common/utils/regexp.unit.test.ts index 26613230d7ac..8b2214de11ba 100644 --- a/src/test/common/utils/regexp.unit.test.ts +++ b/src/test/common/utils/regexp.unit.test.ts @@ -3,8 +3,6 @@ 'use strict'; -// tslint:disable:no-multiline-string - import { expect } from 'chai'; import { verboseRegExp } from '../../../client/common/utils/regexp'; @@ -30,15 +28,15 @@ suite('Utils for regular expressions - verboseRegExp()', () => { [ `spam eggs`, - 'spameggs' + 'spameggs', ], // empty [' ', '(?:)'], [ ` `, - '(?:)' - ] + '(?:)', + ], ]; for (const [pat, expected] of whitespaceTests) { test(`whitespace removed ("${pat}")`, () => { @@ -61,7 +59,7 @@ suite('Utils for regular expressions - verboseRegExp()', () => { '', ` `, - ' ' + ' ', ]; for (const pat of emptyPatterns) { test(`no pattern ("${pat}")`, () => { diff --git a/src/test/common/utils/text.unit.test.ts b/src/test/common/utils/text.unit.test.ts index a26a1fbeadb0..7e7a22896e9a 100644 --- a/src/test/common/utils/text.unit.test.ts +++ b/src/test/common/utils/text.unit.test.ts @@ -3,11 +3,9 @@ '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'; +import { getDedentedLines, getIndent, parsePosition, parseRange } from '../../../client/common/utils/text'; suite('parseRange()', () => { test('valid strings', async () => { @@ -23,12 +21,12 @@ suite('parseRange()', () => { '1:3-', new Range( new Position(1, 3), - new Position(0, 0) // ??? - ) + 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))] + ['3-1', new Range(new Position(3, 0), new Position(1, 0))], ]; for (const [raw, expected] of tests) { const result = parseRange(raw); @@ -64,7 +62,7 @@ suite('parseRange()', () => { 'a-b', 'a', 'a:1', - 'a:b' + 'a:b', ]; for (const raw of tests) { expect(() => parseRange(raw)).to.throw(); @@ -77,7 +75,7 @@ suite('parsePosition()', () => { const tests: [string, Position][] = [ ['1:5', new Position(1, 5)], ['1', new Position(1, 0)], - ['', new Position(0, 0)] + ['', new Position(0, 0)], ]; for (const [raw, expected] of tests) { const result = parsePosition(raw); @@ -100,3 +98,52 @@ suite('parsePosition()', () => { } }); }); + +suite('getIndent()', () => { + const testsData = [ + { line: 'text', expected: '' }, + { line: ' text', expected: ' ' }, + { line: ' text', expected: ' ' }, + { line: ' tabulatedtext', expected: '' }, + ]; + + testsData.forEach((testData) => { + test(`getIndent when line is ${testData.line}`, () => { + const indent = getIndent(testData.line); + + expect(indent).equal(testData.expected); + }); + }); +}); + +suite('getDedentedLines()', () => { + const testsData = [ + { text: '', expected: [] }, + { text: '\n', expected: Error, exceptionMessage: 'expected "first" line to not be blank' }, + { text: 'line1\n', expected: Error, exceptionMessage: 'expected actual first line to be blank' }, + { + text: '\n line2\n line3', + expected: Error, + exceptionMessage: 'line 1 has less indent than the "first" line', + }, + { + text: '\n line2\n line3', + expected: ['line2', 'line3'], + }, + { + text: '\n line2\n line3', + expected: ['line2', ' line3'], + }, + ]; + + testsData.forEach((testData) => { + test(`getDedentedLines when line is ${testData.text}`, () => { + if (Array.isArray(testData.expected)) { + const dedentedLines = getDedentedLines(testData.text); + expect(dedentedLines).to.deep.equal(testData.expected); + } else { + expect(() => getDedentedLines(testData.text)).to.throw(testData.expected, testData.exceptionMessage); + } + }); + }); +}); 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..3541b9b82926 --- /dev/null +++ b/src/test/common/utils/version.unit.test.ts @@ -0,0 +1,348 @@ +// 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( + major: any, + minor: any = NOT_USED, + micro: any = NOT_USED, + + 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) { + ((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.strictEqual(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.strictEqual(result, false); + }); + }); + + INVALID.forEach((data: VersionInfo) => { + const info = data; + test(`bogus: ${info.major}`, () => { + const result = isVersionInfoEmpty(info); + + assert.strictEqual(result, false); + }); + }); +}); + +suite('common utils - normalizeVersionInfo', () => { + suite('valid', () => { + test(`noop`, () => { + const info = ver(1, 2, 3); + info.raw = '1.2.3'; + + ((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'; + + 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; + + ((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; + } + + ((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.strictEqual(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..6f450b8641bc --- /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 { createRunningWorkerPool, QueuePosition } from '../../../client/common/utils/workerPool'; + +suite('Process Queue', () => { + test('Run two workers to calculate square', async () => { + const workerPool = createRunningWorkerPool<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 = createRunningWorkerPool<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 = createRunningWorkerPool<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 as Error); + } + } + 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 = createRunningWorkerPool<number, number>((i) => Promise.resolve(i * i)); + workerPool.stop(); + const reasons: Error[] = []; + try { + await workerPool.addToQueue(2); + } catch (reason) { + reasons.push(reason as Error); + } + assert.deepEqual(reasons, [Error('Queue is stopped')]); + }); + + test('Worker function fails', async () => { + const workerPool = createRunningWorkerPool<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 as string); + } + } + 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 = createRunningWorkerPool<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 index 5035c9316d20..3ba073d71474 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -4,23 +4,16 @@ 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 { anything } 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 { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; 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'; @@ -28,44 +21,47 @@ import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } fr import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { MockProcess } from '../../mocks/process'; import { UnitTestIocContainer } from '../../testing/serviceRegistry'; +import { createTypeMoq } from '../../mocks/helper'; -use(chaiAsPromised); +use(chaiAsPromised.default); 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; + const pathVariableName = getSearchPathEnvVarNames()[0]; suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); + this.skip(); } await clearPythonPathInWorkspaceFolder(workspace4Path); await updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); await initialize(); }); - setup(() => { + setup(async () => { 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(); + await ioc.registerMockInterpreterTypes(); + const mockEnvironmentActivationService = createTypeMoq<IEnvironmentActivationService>(); + mockEnvironmentActivationService + .setup((m) => m.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve({})); + if (ioc.serviceManager.tryGet<IEnvironmentActivationService>(IEnvironmentActivationService)) { + ioc.serviceManager.rebindInstance<IEnvironmentActivationService>( + IEnvironmentActivationService, + mockEnvironmentActivationService.object, + ); + } else { + ioc.serviceManager.addSingletonInstance( + IEnvironmentActivationService, + mockEnvironmentActivationService.object, + ); + } return initializeTest(); }); suiteTeardown(closeActiveWindows); @@ -75,7 +71,6 @@ suite('Multiroot Environment Variables Provider', () => { await clearPythonPathInWorkspaceFolder(workspace4Path); await updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); await initializeTest(); - clearCache(); }); function getVariablesProvider(mockVariables: EnvironmentVariables = { ...process.env }) { @@ -85,16 +80,13 @@ suite('Multiroot Environment Variables Provider', () => { 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 ); } @@ -106,7 +98,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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) { @@ -121,7 +112,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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) { @@ -139,13 +129,12 @@ suite('Multiroot Environment Variables Provider', () => { // 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' + '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) { @@ -163,7 +152,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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'; @@ -177,7 +165,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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'; @@ -196,10 +183,9 @@ suite('Multiroot Environment Variables Provider', () => { // 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(); + 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'; @@ -214,7 +200,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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'; @@ -232,7 +217,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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) { @@ -254,7 +238,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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'; @@ -269,7 +252,6 @@ suite('Multiroot Environment Variables Provider', () => { }); 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) { @@ -292,7 +274,7 @@ suite('Multiroot Environment Variables Provider', () => { 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); @@ -310,7 +292,7 @@ suite('Multiroot Environment Variables Provider', () => { 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); @@ -323,9 +305,9 @@ suite('Multiroot Environment Variables Provider', () => { 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) { @@ -338,7 +320,7 @@ suite('Multiroot Environment Variables Provider', () => { 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. diff --git a/src/test/common/variables/envVarsService.functional.test.ts b/src/test/common/variables/envVarsService.functional.test.ts index 6feac75ee532..3cf55eddbd45 100644 --- a/src/test/common/variables/envVarsService.functional.test.ts +++ b/src/test/common/variables/envVarsService.functional.test.ts @@ -3,8 +3,6 @@ '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'; @@ -15,7 +13,7 @@ import { EnvironmentVariablesService } from '../../../client/common/variables/en import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; import { getOSType } from '../../common'; -use(chaiAsPromised); +use(chaiAsPromised.default); // Functional tests that run code using the VS Code API are found // in envVarsService.test.ts. diff --git a/src/test/common/variables/envVarsService.test.ts b/src/test/common/variables/envVarsService.test.ts index dbacdb4887fe..c7151a8e33b9 100644 --- a/src/test/common/variables/envVarsService.test.ts +++ b/src/test/common/variables/envVarsService.test.ts @@ -3,8 +3,6 @@ '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'; @@ -16,7 +14,7 @@ import { EnvironmentVariablesService } from '../../../client/common/variables/en import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; import { getOSType } from '../../common'; -use(chaiAsPromised); +use(chaiAsPromised.default); const envFilesFolderPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc', 'workspace4'); @@ -76,7 +74,7 @@ suite('Environment Variables Service', () => { test('Simple variable substitution is supported', async () => { const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env6'), { - BINDIR: '/usr/bin' + BINDIR: '/usr/bin', }); expect(vars).to.not.equal(undefined, 'Variables is undefiend'); @@ -85,7 +83,7 @@ suite('Environment Variables Service', () => { expect(vars).to.have.property( 'PYTHONPATH', '/home/user/git/foobar/foo:/home/user/git/foobar/bar', - 'value is invalid' + '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 index 0e1ae863478d..3709d97b9f62 100644 --- a/src/test/common/variables/envVarsService.unit.test.ts +++ b/src/test/common/variables/envVarsService.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -12,17 +10,16 @@ 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'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; -use(chaiAsPromised); +use(chaiAsPromised.default); type PathVar = 'Path' | 'PATH'; -const PATHS = [ - 'Path', // Windows - 'PATH' // non-Windows -]; +const PATHS = getSearchPathEnvVarNames(); suite('Environment Variables Service', () => { const filename = 'x/y/z/.env'; + const processEnvPath = getSearchPathEnvVarNames()[0]; let pathUtils: TypeMoq.IMock<IPathUtils>; let fs: TypeMoq.IMock<IFileSystem>; let variablesService: EnvironmentVariablesService; @@ -32,7 +29,7 @@ suite('Environment Variables Service', () => { variablesService = new EnvironmentVariablesService( // This is the only place that the mocks are used. pathUtils.object, - fs.object + fs.object, ); }); function verifyAll() { @@ -40,7 +37,7 @@ suite('Environment Variables Service', () => { fs.verifyAll(); } function setFile(fileName: string, text: string) { - fs.setup((f) => f.fileExists(fileName)) // Handle the specific file. + fs.setup((f) => f.pathExists(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. @@ -55,7 +52,7 @@ suite('Environment Variables Service', () => { }); test('Custom variables should be undefined with non-existent files', async () => { - fs.setup((f) => f.fileExists(filename)) // Handle the specific file. + fs.setup((f) => f.pathExists(filename)) // Handle the specific file. .returns(() => Promise.resolve(false)); // The file is missing. const vars = await variablesService.parseFile(filename); @@ -66,7 +63,7 @@ suite('Environment Variables Service', () => { 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". + fs.setup((f) => f.pathExists(dirname)) // Handle the specific "file". .returns(() => Promise.resolve(false)); // It isn't a "regular" file. const vars = await variablesService.parseFile(dirname); @@ -91,7 +88,7 @@ suite('Environment Variables Service', () => { ` X1234PYEXTUNITTESTVAR=1234 PYTHONPATH=../workspace5 - ` + `, ); const vars = await variablesService.parseFile(filename); @@ -117,7 +114,7 @@ PYTHONPATH=/usr/one/three:/usr/one/four PATH=/usr/x:/usr/y # Windows Path variable Path=/usr/x:/usr/y - ` + `, ); const vars = await variablesService.parseFile(filename); @@ -135,13 +132,12 @@ Path=/usr/x:/usr/y // 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' }); @@ -152,7 +148,7 @@ PYTHON=${BINDIR}/python3\n\ expect(vars).to.have.property( 'PYTHONPATH', '/home/user/git/foobar/foo:/home/user/git/foobar/bar', - 'value is invalid' + 'value is invalid', ); expect(vars).to.have.property('PYTHON', '/usr/bin/python3', 'value is invalid'); verifyAll(); @@ -183,7 +179,7 @@ PYTHON=${BINDIR}/python3\n\ 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' }; @@ -197,10 +193,10 @@ PYTHON=${BINDIR}/python3\n\ verifyAll(); }); - test('Ensure path variables variables in target are left untouched', async () => { + test('Ensure path 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); @@ -211,7 +207,25 @@ PYTHON=${BINDIR}/python3\n\ 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'); + expect(vars2).to.have.property(processEnvPath, 'PATH', 'Incorrect value'); + verifyAll(); + }); + + test('Ensure path variables in target are overwritten', async () => { + const source = { ONE: '1', TWO: 'TWO' }; + const target = { ONE: 'ONE', THREE: '3', PYTHONPATH: 'PYTHONPATH' }; + + (target as any)[pathVariable] = 'PATH'; + + variablesService.mergeVariables(source, target, { overwrite: true }); + + expect(Object.keys(source)).lengthOf(2, 'Source variables modified'); + expect(Object.keys(target)).lengthOf(5, 'Variables not merged'); + expect(target).to.have.property('ONE', '1', 'Expected to be overwritten'); + expect(target).to.have.property('TWO', 'TWO', 'Incorrect value'); + expect(target).to.have.property('THREE', '3', 'Variable not merged'); + expect(target).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + expect(target).to.have.property(processEnvPath, 'PATH', 'Incorrect value'); verifyAll(); }); }); @@ -245,30 +259,30 @@ PYTHON=${BINDIR}/python3\n\ 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'); + expect(vars).to.have.property(processEnvPath, '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'); + expect(vars).to.have.property(processEnvPath, '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'); + expect(vars).to.have.property(processEnvPath, '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`; @@ -276,7 +290,11 @@ PYTHON=${BINDIR}/python3\n\ 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'); + expect(vars).to.have.property( + processEnvPath, + `PATH${path.delimiter}${pathToAppend}`, + 'Incorrect value', + ); verifyAll(); }); }); @@ -333,7 +351,7 @@ PYTHON=${BINDIR}/python3\n\ expect(vars).to.have.property( 'PYTHONPATH', `PYTHONPATH${path.delimiter}${pathToAppend}`, - 'Incorrect value' + 'Incorrect value', ); verifyAll(); }); @@ -343,7 +361,6 @@ PYTHON=${BINDIR}/python3\n\ 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 @@ -356,7 +373,6 @@ PYTHONPATH=../workspace5 }); 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 @@ -378,12 +394,10 @@ Path=/usr/x:/usr/y }); 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=... @@ -391,19 +405,20 @@ bogus-5=... bogus~6=... VAR1=3456 VAR_2=7890 +_VAR_3=1234 `); expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(Object.keys(vars!)).lengthOf(6, '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'); + expect(vars).to.have.property('_VAR_3', '1234', 'value is invalid'); }); test('Empty values become empty string', () => { - // tslint:disable-next-line:no-multiline-string const vars = parseEnvFile(` SPAM= `); @@ -413,40 +428,37 @@ SPAM= expect(vars).to.have.property('SPAM', '', 'value is invalid'); }); - test('Outer quotation marks are removed', () => { - // tslint:disable-next-line:no-multiline-string + test('Outer quotation marks are removed and cause newline substitution', () => { const vars = parseEnvFile(` -SPAM=1234 -HAM='5678' -EGGS="9012" -FOO='"3456"' -BAR="'7890'" -BAZ="\"ABCD" -VAR1="EFGH -VAR2=IJKL" +SPAM=12\\n34 +HAM='56\\n78' +EGGS="90\\n12" +FOO='"34\\n56"' +BAR="'78\\n90'" +BAZ="\"AB\\nCD" +VAR1="EF\\nGH +VAR2=IJ\\nKL" 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 + expect(vars).to.have.property('SPAM', '12\\n34', 'value is invalid'); + expect(vars).to.have.property('HAM', '56\n78', 'value is invalid'); + expect(vars).to.have.property('EGGS', '90\n12', 'value is invalid'); + expect(vars).to.have.property('FOO', '"34\n56"', 'value is invalid'); + expect(vars).to.have.property('BAR', "'78\n90'", 'value is invalid'); + expect(vars).to.have.property('BAZ', '"AB\nCD', 'value is invalid'); + expect(vars).to.have.property('VAR1', '"EF\\nGH', 'value is invalid'); + expect(vars).to.have.property('VAR2', 'IJ\\nKL"', 'value is invalid'); + // 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 @@ -458,7 +470,6 @@ 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'); @@ -474,8 +485,6 @@ VAR3=' MNOP ' }); test('Blank lines are ignored', () => { - // tslint:disable:no-trailing-whitespace - // tslint:disable-next-line:no-multiline-string const vars = parseEnvFile(` SPAM=1234 @@ -484,7 +493,6 @@ 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'); @@ -493,7 +501,6 @@ HAM=5678 }); test('Comments are ignored', () => { - // tslint:disable-next-line:no-multiline-string const vars = parseEnvFile(` # step 1 SPAM=1234 @@ -512,15 +519,12 @@ EGGS=9012 # ... }); 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'); @@ -529,12 +533,27 @@ PYTHONPATH=${REPO}/foo:${REPO}/bar \n\ expect(vars).to.have.property( 'PYTHONPATH', '/home/user/git/foobar/foo:/home/user/git/foobar/bar', - 'value is invalid' + 'value is invalid', + ); + }); + + test('Example from docs', () => { + const vars = parseEnvFile( + '\ +VAR1=abc \n\ +VAR2_A="${VAR1}\\ndef" \n\ +VAR2_B="${VAR1}\\n"def \n\ + ', ); + + expect(vars).to.not.equal(undefined, 'Variables is undefined'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('VAR1', 'abc', 'value is invalid'); + expect(vars).to.have.property('VAR2_A', 'abc\ndef', 'value is invalid'); + expect(vars).to.have.property('VAR2_B', '"abc\\n"def', '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\ @@ -547,7 +566,6 @@ EGGS=$SPAM \n\ }); test('Nested substitution is not supported', () => { - // tslint:disable-next-line:no-multiline-string const vars = parseEnvFile( '\ SPAM=EGGS \n\ @@ -557,7 +575,7 @@ abcEGGSxyz=!!! \n\ HAM2="-- ${abc${SPAM}xyz} --"\n\ HAM3="-- ${${SPAM} --"\n\ HAM4="-- ${${SPAM}} ${EGGS} --"\n\ - ' + ', ); expect(vars).to.not.equal(undefined, 'Variables is undefiend'); @@ -572,7 +590,6 @@ HAM4="-- ${${SPAM}} ${EGGS} --"\n\ }); test('Other bad substitution syntax', () => { - // tslint:disable-next-line:no-multiline-string const vars = parseEnvFile( '\ SPAM=EGGS \n\ @@ -581,7 +598,7 @@ HAM1=${} \n\ HAM2=${ \n\ HAM3=${SPAM+EGGS} \n\ HAM4=$SPAM \n\ - ' + ', ); expect(vars).to.not.equal(undefined, 'Variables is undefiend'); @@ -595,13 +612,12 @@ HAM4=$SPAM \n\ }); 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'); @@ -610,33 +626,33 @@ PYTHONPATH=${PYTHONPATH}:${REPO}/bar \n\ expect(vars).to.have.property( 'PYTHONPATH', '/home/user/git/foobar/foo:/home/user/git/foobar/bar', - 'value is invalid' + 'value is invalid', ); }); - test('Substitution may be escaped', () => { - // tslint:disable-next-line:no-multiline-string + test('"$" may be escaped', () => { const vars = parseEnvFile( '\ SPAM=1234 \n\ EGGS=\\${SPAM}/foo:\\${SPAM}/bar \n\ HAM=$ ... $$ \n\ - ' +FOO=foo\\$bar \n\ + ', ); expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(Object.keys(vars!)).lengthOf(4, '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'); + expect(vars).to.have.property('FOO', 'foo$bar', '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' + REPO: '/home/user/git/foobar', }); expect(vars).to.not.equal(undefined, 'Variables is undefiend'); @@ -644,11 +660,9 @@ PYTHONPATH=${REPO}/foo:${REPO}/bar \n\ expect(vars).to.have.property( 'PYTHONPATH', '/home/user/git/foobar/foo:/home/user/git/foobar/bar', - 'value is invalid' + '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 index 5faa09400240..bf02f5f867d7 100644 --- a/src/test/common/variables/environmentVariablesProvider.unit.test.ts +++ b/src/test/common/variables/environmentVariablesProvider.unit.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -11,57 +12,45 @@ 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 { ICurrentProcess } 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; + let envFile: string; setup(() => { + envFile = ''; 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); + when(workspace.getConfiguration('python', anything())).thenReturn({ + get: (settingName: string) => (settingName === 'envFile' ? envFile : ''), + } as any); provider = new EnvironmentVariablesProvider( instance(envVarsService), [], instance(platform), instance(workspace), - instance(configuration), instance(currentProcess), - instance(serviceContainer) ); sinon.stub(EnvFileTelemetry, 'sendFileCreationTelemetry').returns(); - - clearCache(); }); teardown(() => { @@ -75,62 +64,73 @@ suite('Multiroot Environment Variables Provider', () => { provider.trackedWorkspaceFolders.add(workspaceFolder1Uri.fsPath); provider.trackedWorkspaceFolders.add(workspaceFolder2Uri.fsPath); - provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + 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); + assert.strictEqual(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)); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); const changedEvent: ConfigurationChangeEvent = { - affectsConfiguration(_setting: string, _uri?: Uri) { + affectsConfiguration() { return false; - } + }, }; provider.configurationChanged(changedEvent); - assert.equal(affectedWorkspace, undefined); + assert.strictEqual(affectedWorkspace, undefined); }); test('Event is not fired when workspace is not tracked', () => { let affectedWorkspace: Uri | undefined; - provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); const changedEvent: ConfigurationChangeEvent = { - affectsConfiguration(_setting: string, _uri?: Uri) { + affectsConfiguration() { return true; - } + }, }; provider.configurationChanged(changedEvent); - assert.equal(affectedWorkspace, undefined); + assert.strictEqual(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'); + envFile = path.join('a', 'b', 'env.file'); const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + // eslint-disable-next-line @typescript-eslint/ban-types let onChangeHandler: undefined | ((resource?: Uri) => Function); fileSystemWatcher .setup((fs) => fs.onDidChange(typemoq.It.isAny())) - .callback((cb) => (onChangeHandler = cb)) + .callback((cb) => { + onChangeHandler = cb; + }) .verifiable(typemoq.Times.once()); when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); - provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); provider.createFileWatcher(envFile, workspaceUri); @@ -139,21 +139,26 @@ suite('Multiroot Environment Variables Provider', () => { onChangeHandler!(); - assert.equal(affectedWorkspace, workspaceUri); + assert.strictEqual(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'); + envFile = path.join('a', 'b', 'env.file'); const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + // eslint-disable-next-line @typescript-eslint/ban-types let onDeleted: undefined | ((resource?: Uri) => Function); fileSystemWatcher .setup((fs) => fs.onDidDelete(typemoq.It.isAny())) - .callback((cb) => (onDeleted = cb)) + .callback((cb) => { + onDeleted = cb; + }) .verifiable(typemoq.Times.once()); when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); - provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); provider.createFileWatcher(envFile, workspaceUri); @@ -162,21 +167,26 @@ suite('Multiroot Environment Variables Provider', () => { onDeleted!(); - assert.equal(affectedWorkspace, workspaceUri); + assert.strictEqual(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'); + envFile = path.join('a', 'b', 'env.file'); const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + // eslint-disable-next-line @typescript-eslint/ban-types let onCreated: undefined | ((resource?: Uri) => Function); fileSystemWatcher .setup((fs) => fs.onDidCreate(typemoq.It.isAny())) - .callback((cb) => (onCreated = cb)) + .callback((cb) => { + onCreated = cb; + }) .verifiable(typemoq.Times.once()); when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); - provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); provider.createFileWatcher(envFile, workspaceUri); @@ -185,10 +195,10 @@ suite('Multiroot Environment Variables Provider', () => { onCreated!(); - assert.equal(affectedWorkspace, workspaceUri); + assert.strictEqual(affectedWorkspace, workspaceUri); }); test(`File system watcher event handlers are added once ${workspaceTitle}`, () => { - const envFile = path.join('a', 'b', 'env.file'); + 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()); @@ -207,12 +217,11 @@ suite('Multiroot Environment Variables Provider', () => { }); 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'); + 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'); @@ -220,49 +229,44 @@ suite('Multiroot Environment Variables Provider', () => { 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'); + 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(envVarsService.parseFile(envFile, currentProcEnv)).thenCall(async () => ({ ...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'); + 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(envVarsService.parseFile(envFile, currentProcEnv)).thenCall(async () => ({ ...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(); @@ -272,12 +276,11 @@ suite('Multiroot Environment Variables Provider', () => { }); test(`Getting environment variables which are already cached does not reinvoke the method ${workspaceTitle}`, async () => { - const envFile = path.join('a', 'b', 'env.file'); + 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'); @@ -289,17 +292,16 @@ suite('Multiroot Environment Variables Provider', () => { await provider.getEnvironmentVariables(workspaceUri); // Verify that the contents of `_getEnvironmentVariables()` method are only invoked once - verify(configuration.getSettings(anything())).once(); + verify(workspace.getConfiguration('python', anything())).once(); assert.deepEqual(vars, {}); }); test(`Cache result must be cleared when cache expires ${workspaceTitle}`, async () => { - const envFile = path.join('a', 'b', 'env.file'); + 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'); @@ -309,10 +311,8 @@ suite('Multiroot Environment Variables Provider', () => { [], instance(platform), instance(workspace), - instance(configuration), instance(currentProcess), - instance(serviceContainer), - 100 + 100, ); const vars = await provider.getEnvironmentVariables(workspaceUri); @@ -322,8 +322,64 @@ suite('Multiroot Environment Variables Provider', () => { await provider.getEnvironmentVariables(workspaceUri); // Verify that the contents of `_getEnvironmentVariables()` method are invoked twice - verify(configuration.getSettings(anything())).twice(); + verify(workspace.getConfiguration('python', anything())).twice(); assert.deepEqual(vars, {}); }); + + test(`Environment variables are updated when env file changes ${workspaceTitle}`, async () => { + const root = workspaceUri?.fsPath ?? ''; + const sourceDir = path.join(root, 'a', 'b'); + envFile = path.join(sourceDir, 'env.file'); + const sourceFile = path.join(sourceDir, 'main.py'); + + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { + SOMETHING: 'wow', + PATH: 'some path value', + }; + const envFileVars = { MY_FILE: '1234', PYTHONPATH: `./foo${path.delimiter}./bar` }; + + // eslint-disable-next-line @typescript-eslint/ban-types + let onChangeHandler: undefined | ((resource?: Uri) => Function); + const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + + fileSystemWatcher + .setup((fs) => fs.onDidChange(typemoq.It.isAny())) + .callback((cb) => { + onChangeHandler = cb; + }) + .verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + + when(currentProcess.env).thenReturn(currentProcEnv); + when(workspace.getWorkspaceFolder(anything())).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenCall(async () => ({ ...envFileVars })); + when(platform.pathVariableName).thenReturn('PATH'); + + provider.createFileWatcher(envFile, undefined); + + fileSystemWatcher.verifyAll(); + assert.ok(onChangeHandler); + + async function checkVars() { + let vars = await provider.getEnvironmentVariables(undefined); + assert.deepEqual(vars, envFileVars); + + vars = await provider.getEnvironmentVariables(Uri.file(sourceFile)); + assert.deepEqual(vars, envFileVars); + + vars = await provider.getEnvironmentVariables(Uri.file(sourceDir)); + assert.deepEqual(vars, envFileVars); + } + + await checkVars(); + + envFileVars.MY_FILE = 'CHANGED'; + envFileVars.PYTHONPATH += 'CHANGED'; + + onChangeHandler!(); + + await checkVars(); + }); }); }); diff --git a/src/test/common/variables/serviceRegistry.unit.test.ts b/src/test/common/variables/serviceRegistry.unit.test.ts index 55a3bbc2f2a4..541579c609f7 100644 --- a/src/test/common/variables/serviceRegistry.unit.test.ts +++ b/src/test/common/variables/serviceRegistry.unit.test.ts @@ -23,14 +23,14 @@ suite('Common variables Service Registry', () => { verify( serviceManager.addSingleton<IEnvironmentVariablesService>( IEnvironmentVariablesService, - EnvironmentVariablesService - ) + EnvironmentVariablesService, + ), ).once(); verify( serviceManager.addSingleton<IEnvironmentVariablesProvider>( IEnvironmentVariablesProvider, - EnvironmentVariablesProvider - ) + EnvironmentVariablesProvider, + ), ).once(); }); }); diff --git a/src/test/configuration/environmentTypeComparer.unit.test.ts b/src/test/configuration/environmentTypeComparer.unit.test.ts new file mode 100644 index 000000000000..bce20fcb0fef --- /dev/null +++ b/src/test/configuration/environmentTypeComparer.unit.test.ts @@ -0,0 +1,398 @@ +// 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 { Architecture } from '../../client/common/utils/platform'; +import { + EnvironmentTypeComparer, + EnvLocationHeuristic, + getEnvLocationHeuristic, +} from '../../client/interpreter/configuration/environmentTypeComparer'; +import { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { PythonEnvType } from '../../client/pythonEnvironments/base/info'; +import * as pyenv from '../../client/pythonEnvironments/common/environmentManagers/pyenv'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; + +suite('Environment sorting', () => { + const workspacePath = path.join('path', 'to', 'workspace'); + let interpreterHelper: IInterpreterHelper; + let getActiveWorkspaceUriStub: sinon.SinonStub; + let getInterpreterTypeDisplayNameStub: sinon.SinonStub; + const preferredPyenv = path.join('path', 'to', 'preferred', 'pyenv'); + + setup(() => { + getActiveWorkspaceUriStub = sinon.stub().returns({ folderUri: { fsPath: workspacePath } }); + getInterpreterTypeDisplayNameStub = sinon.stub(); + + interpreterHelper = ({ + getActiveWorkspaceUri: getActiveWorkspaceUriStub, + getInterpreterTypeDisplayName: getInterpreterTypeDisplayNameStub, + } as unknown) as IInterpreterHelper; + const getActivePyenvForDirectory = sinon.stub(pyenv, 'getActivePyenvForDirectory'); + getActivePyenvForDirectory.resolves(preferredPyenv); + }); + + teardown(() => { + sinon.restore(); + }); + + type ComparisonTestCaseType = { + title: string; + envA: PythonEnvironment; + envB: PythonEnvironment; + expected: number; + }; + + const testcases: ComparisonTestCaseType[] = [ + { + title: 'Local virtual environment should come first', + envA: { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.System, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: -1, + }, + { + title: "Non-local virtual environment should not come first when there's a local env", + envA: { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join('path', 'to', 'other', 'workspace', '.venv'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: "Conda environment should not come first when there's a local env", + envA: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Conda base environment should come after any other conda env', + envA: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envName: 'base', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envName: 'random-name', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Pipenv environment should come before any other conda env', + envA: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envName: 'conda-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pipenv, + envName: 'pipenv-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + + expected: 1, + }, + { + title: 'System environment should not come first when there are global envs', + envA: { + envType: EnvironmentType.System, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Poetry, + type: PythonEnvType.Virtual, + envName: 'poetry-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Pyenv interpreter should not come first when there are global envs', + envA: { + envType: EnvironmentType.Pyenv, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, + envName: 'pipenv-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Preferred Pyenv interpreter should come before any global interpreter', + envA: { + envType: EnvironmentType.Pyenv, + version: { major: 3, minor: 12, patch: 2 }, + path: preferredPyenv, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pyenv, + version: { major: 3, minor: 10, patch: 2 }, + path: path.join('path', 'to', 'normal', 'pyenv'), + } as PythonEnvironment, + expected: -1, + }, + { + title: 'Pyenv interpreters should come first when there are global interpreters', + envA: { + envType: EnvironmentType.Global, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pyenv, + version: { major: 3, minor: 7, patch: 2 }, + path: path.join('path', 'to', 'normal', 'pyenv'), + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Global environment should not come first when there are global envs', + envA: { + envType: EnvironmentType.Global, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Poetry, + type: PythonEnvType.Virtual, + envName: 'poetry-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Microsoft Store environment should not come first when there are global envs', + envA: { + envType: EnvironmentType.MicrosoftStore, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.VirtualEnv, + type: PythonEnvType.Virtual, + envName: 'virtualenv-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: + 'Microsoft Store interpreter should not come first when there are global interpreters with higher version', + envA: { + envType: EnvironmentType.MicrosoftStore, + version: { major: 3, minor: 10, patch: 2, raw: '3.10.2' }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Global, + version: { major: 3, minor: 11, patch: 2, raw: '3.11.2' }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Unknown environment should not come first when there are global envs', + envA: { + envType: EnvironmentType.Unknown, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, + envName: 'pipenv-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'If 2 environments are of the same type, the most recent Python version comes first', + envA: { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join(workspacePath, '.old-venv'), + version: { major: 3, minor: 7, patch: 5, raw: '3.7.5' }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 2, raw: '3.10.2' }, + } as PythonEnvironment, + expected: 1, + }, + { + title: + "If 2 global environments have the same Python version and there's a Conda one, the Conda env should not come first", + envA: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envName: 'conda-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, + envName: 'pipenv-env', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: + 'If 2 global environments are of the same type and have the same Python version, they should be sorted by name', + envA: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envName: 'conda-foo', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envName: 'conda-bar', + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'If 2 global interpreters have the same Python version, they should be sorted by architecture', + envA: { + envType: EnvironmentType.Global, + architecture: Architecture.x86, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Global, + architecture: Architecture.x64, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + { + title: 'Problematic environments should come last', + envA: { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envPath: path.join(workspacePath, '.venv'), + path: 'python', + } as PythonEnvironment, + envB: { + envType: EnvironmentType.System, + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment, + expected: 1, + }, + ]; + + testcases.forEach(({ title, envA, envB, expected }) => { + test(title, async () => { + const envTypeComparer = new EnvironmentTypeComparer(interpreterHelper); + await envTypeComparer.initialize(undefined); + const result = envTypeComparer.compare(envA, envB); + + assert.strictEqual(result, expected); + }); + }); +}); + +suite('getEnvTypeHeuristic tests', () => { + const workspacePath = path.join('path', 'to', 'workspace'); + + const localGlobalEnvTypes = [ + EnvironmentType.Venv, + EnvironmentType.Conda, + EnvironmentType.VirtualEnv, + EnvironmentType.VirtualEnvWrapper, + EnvironmentType.Pipenv, + EnvironmentType.Poetry, + ]; + + localGlobalEnvTypes.forEach((envType) => { + test('If the path to an environment starts with the workspace path it should be marked as local', () => { + const environment = { + envType, + envPath: path.join(workspacePath, 'my-environment'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvLocationHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvLocationHeuristic.Local); + }); + + test('If the path to an environment does not start with the workspace path it should be marked as global', () => { + const environment = { + envType, + envPath: path.join('path', 'to', 'my-environment'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvLocationHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvLocationHeuristic.Global); + }); + + test('If envPath is not set, fallback to path', () => { + const environment = { + envType, + path: path.join(workspacePath, 'my-environment'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvLocationHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvLocationHeuristic.Local); + }); + }); + + const globalInterpretersEnvTypes = [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Unknown, + EnvironmentType.Pyenv, + ]; + + globalInterpretersEnvTypes.forEach((envType) => { + test(`If the environment type is ${envType} and the environment path does not start with the workspace path it should be marked as a global interpreter`, () => { + const environment = { + envType, + envPath: path.join('path', 'to', 'a', 'global', 'interpreter'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvLocationHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvLocationHeuristic.Global); + }); + }); +}); diff --git a/src/test/configuration/interpreterSelector/commands/installPython.unit.test.ts b/src/test/configuration/interpreterSelector/commands/installPython.unit.test.ts new file mode 100644 index 000000000000..bed3397a0324 --- /dev/null +++ b/src/test/configuration/interpreterSelector/commands/installPython.unit.test.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { SemVer } from 'semver'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ExtensionContextKey } from '../../../../client/common/application/contextKeys'; +import { ICommandManager, IContextKeyManager } from '../../../../client/common/application/types'; +import { PythonWelcome } from '../../../../client/common/application/walkThroughs'; +import { Commands, PVSC_EXTENSION_ID } from '../../../../client/common/constants'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { IBrowserService, IDisposable } from '../../../../client/common/types'; +import { InstallPythonCommand } from '../../../../client/interpreter/configuration/interpreterSelector/commands/installPython'; + +suite('Install Python Command', () => { + let cmdManager: ICommandManager; + let browserService: IBrowserService; + let contextKeyManager: IContextKeyManager; + let platformService: IPlatformService; + let installPythonCommand: InstallPythonCommand; + let walkthroughID: + | { + category: string; + step: string; + } + | undefined; + setup(() => { + walkthroughID = undefined; + cmdManager = mock<ICommandManager>(); + when(cmdManager.executeCommand('workbench.action.openWalkthrough', anything(), false)).thenCall((_, w) => { + walkthroughID = w; + }); + browserService = mock<IBrowserService>(); + when(browserService.launch(anything())).thenReturn(undefined); + contextKeyManager = mock<IContextKeyManager>(); + when(contextKeyManager.setContext(ExtensionContextKey.showInstallPythonTile, true)).thenResolve(); + platformService = mock<IPlatformService>(); + installPythonCommand = new InstallPythonCommand( + instance(cmdManager), + instance(contextKeyManager), + instance(browserService), + instance(platformService), + [], + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure command is registered with the correct callback handler', async () => { + let installCommandHandler!: () => Promise<void>; + when(cmdManager.registerCommand(Commands.InstallPython, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + + await installPythonCommand.activate(); + + verify(cmdManager.registerCommand(Commands.InstallPython, anything())).once(); + + const installPython = sinon.stub(InstallPythonCommand.prototype, '_installPython'); + await installCommandHandler(); + assert(installPython.calledOnce); + }); + + test('Opens Linux Install tile on Linux', async () => { + when(platformService.isWindows).thenReturn(false); + when(platformService.isLinux).thenReturn(true); + when(platformService.isMac).thenReturn(false); + const expectedWalkthroughID = { + category: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}`, + step: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}#${PythonWelcome.linuxInstallId}`, + }; + await installPythonCommand._installPython(); + verify(contextKeyManager.setContext(ExtensionContextKey.showInstallPythonTile, true)).once(); + verify(browserService.launch(anything())).never(); + assert.deepEqual(walkthroughID, expectedWalkthroughID); + }); + + test('Opens Mac Install tile on MacOS', async () => { + when(platformService.isWindows).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isMac).thenReturn(true); + const expectedWalkthroughID = { + category: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}`, + step: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}#${PythonWelcome.macOSInstallId}`, + }; + await installPythonCommand._installPython(); + verify(contextKeyManager.setContext(ExtensionContextKey.showInstallPythonTile, true)).once(); + verify(browserService.launch(anything())).never(); + assert.deepEqual(walkthroughID, expectedWalkthroughID); + }); + + test('Opens Windows Install tile on Windows 8', async () => { + when(platformService.isWindows).thenReturn(true); + when(platformService.isLinux).thenReturn(false); + when(platformService.isMac).thenReturn(false); + when(platformService.getVersion()).thenResolve(new SemVer('8.2.0')); + const expectedWalkthroughID = { + category: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}`, + step: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}#${PythonWelcome.windowsInstallId}`, + }; + await installPythonCommand._installPython(); + verify(contextKeyManager.setContext(ExtensionContextKey.showInstallPythonTile, true)).once(); + verify(browserService.launch(anything())).never(); + assert.deepEqual(walkthroughID, expectedWalkthroughID); + }); + + test('Opens microsoft store app on Windows otherwise', async () => { + when(platformService.isWindows).thenReturn(true); + when(platformService.isLinux).thenReturn(false); + when(platformService.isMac).thenReturn(false); + when(platformService.getVersion()).thenResolve(new SemVer('10.0.0')); + await installPythonCommand._installPython(); + verify(browserService.launch(anything())).once(); + verify(contextKeyManager.setContext(ExtensionContextKey.showInstallPythonTile, true)).never(); + }); +}); diff --git a/src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.unit.test.ts b/src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.unit.test.ts new file mode 100644 index 000000000000..16014290c218 --- /dev/null +++ b/src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.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 rewiremock from 'rewiremock'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ICommandManager, ITerminalManager } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { ITerminalService } from '../../../../client/common/terminal/types'; +import { IDisposable } from '../../../../client/common/types'; +import { Interpreters } from '../../../../client/common/utils/localize'; +import { InstallPythonViaTerminal } from '../../../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; + +suite('Install Python via Terminal', () => { + let cmdManager: ICommandManager; + let terminalServiceFactory: ITerminalManager; + let installPythonCommand: InstallPythonViaTerminal; + let terminalService: ITerminalService; + let message: string | undefined; + setup(() => { + rewiremock.enable(); + cmdManager = mock<ICommandManager>(); + terminalServiceFactory = mock<ITerminalManager>(); + terminalService = mock<ITerminalService>(); + message = undefined; + when(terminalServiceFactory.createTerminal(anything())).thenCall((options) => { + message = options.message; + return instance(terminalService); + }); + installPythonCommand = new InstallPythonViaTerminal(instance(cmdManager), instance(terminalServiceFactory), []); + }); + + teardown(() => { + rewiremock.disable(); + sinon.restore(); + }); + + test('Sends expected commands when InstallPythonOnLinux command is executed if apt is available', async () => { + let installCommandHandler: () => Promise<void>; + when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + rewiremock('which').with((cmd: string) => { + if (cmd === 'apt') { + return 'path/to/apt'; + } + throw new Error('Command not found'); + }); + await installPythonCommand.activate(); + when(terminalService.sendText('sudo apt-get update')).thenResolve(); + when(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).thenResolve(); + + await installCommandHandler!(); + + verify(terminalService.sendText('sudo apt-get update')).once(); + verify(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).once(); + }); + + test('Sends expected commands when InstallPythonOnLinux command is executed if dnf is available', async () => { + let installCommandHandler: () => Promise<void>; + when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + rewiremock('which').with((cmd: string) => { + if (cmd === 'dnf') { + return 'path/to/dnf'; + } + throw new Error('Command not found'); + }); + + await installPythonCommand.activate(); + when(terminalService.sendText('sudo dnf install python3')).thenResolve(); + + await installCommandHandler!(); + + verify(terminalService.sendText('sudo dnf install python3')).once(); + expect(message).to.be.equal(undefined); + }); + + test('Creates terminal with appropriate message when InstallPythonOnLinux command is executed if no known linux package managers are available', async () => { + let installCommandHandler: () => Promise<void>; + when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + rewiremock('which').with((_cmd: string) => { + throw new Error('Command not found'); + }); + + await installPythonCommand.activate(); + await installCommandHandler!(); + + expect(message).to.be.equal(Interpreters.installPythonTerminalMessageLinux); + }); + + test('Sends expected commands on Mac when InstallPythonOnMac command is executed if brew is available', async () => { + let installCommandHandler: () => Promise<void>; + when(cmdManager.registerCommand(Commands.InstallPythonOnMac, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + rewiremock('which').with((cmd: string) => { + if (cmd === 'brew') { + return 'path/to/brew'; + } + throw new Error('Command not found'); + }); + + await installPythonCommand.activate(); + when(terminalService.sendText('brew install python3')).thenResolve(); + + await installCommandHandler!(); + + verify(terminalService.sendText('brew install python3')).once(); + expect(message).to.be.equal(undefined); + }); + + test('Creates terminal with appropriate message when InstallPythonOnMac command is executed if brew is not available', async () => { + let installCommandHandler: () => Promise<void>; + when(cmdManager.registerCommand(Commands.InstallPythonOnMac, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + rewiremock('which').with((_cmd: string) => { + throw new Error('Command not found'); + }); + + await installPythonCommand.activate(); + + await installCommandHandler!(); + + expect(message).to.be.equal(Interpreters.installPythonTerminalMacMessage); + }); +}); diff --git a/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts index 1e247f71a9ba..a32c794b7dc7 100644 --- a/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts @@ -2,25 +2,38 @@ // Licensed under the MIT License. import * as path from 'path'; +import * as sinon from 'sinon'; 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 { PathUtils } from '../../../../client/common/platform/pathUtils'; +import { IConfigurationService } from '../../../../client/common/types'; +import { Common, Interpreters } from '../../../../client/common/utils/localize'; import { ResetInterpreterCommand } from '../../../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter'; import { IPythonPathUpdaterServiceManager } from '../../../../client/interpreter/configuration/types'; +import * as extapi from '../../../../client/envExt/api.internal'; -// 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>; + let configurationService: TypeMoq.IMock<IConfigurationService>; const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; let resetInterpreterCommand: ResetInterpreterCommand; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => ({ pythonPath: 'pythonPath' } as any)); commandManager = TypeMoq.Mock.ofType<ICommandManager>(); appShell = TypeMoq.Mock.ofType<IApplicationShell>(); pythonPathUpdater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); @@ -30,11 +43,15 @@ suite('Reset Interpreter Command', () => { pythonPathUpdater.object, commandManager.object, appShell.object, - workspace.object + workspace.object, + new PathUtils(false), + configurationService.object, ); }); + teardown(() => { + sinon.restore(); + }); - // 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); @@ -45,8 +62,8 @@ suite('Reset Interpreter Command', () => { TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.Global), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(undefined) - ) + TypeMoq.It.isValue(undefined), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -68,8 +85,8 @@ suite('Reset Interpreter Command', () => { TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder.uri) - ) + TypeMoq.It.isValue(folder.uri), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -83,20 +100,23 @@ suite('Reset Interpreter Command', () => { 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: Common.clearAll }, { label: 'one', description: path.dirname(folder1.uri.fsPath), - uri: folder1.uri + uri: folder1.uri, + detail: 'pythonPath', }, { label: 'two', description: path.dirname(folder2.uri.fsPath), - uri: folder2.uri + uri: folder2.uri, + detail: 'pythonPath', }, { - label: Interpreters.entireWorkspace(), - uri: folder1.uri - } + label: Interpreters.clearAtWorkspace, + uri: folder1.uri, + }, ]; appShell .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) @@ -104,8 +124,9 @@ suite('Reset Interpreter Command', () => { Promise.resolve({ label: 'two', description: path.dirname(folder2.uri.fsPath), - uri: folder2.uri - }) + uri: folder2.uri, + detail: 'pythonPath', + }), ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater @@ -114,8 +135,8 @@ suite('Reset Interpreter Command', () => { TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder2.uri) - ) + TypeMoq.It.isValue(folder2.uri), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -126,31 +147,34 @@ suite('Reset Interpreter Command', () => { workspace.verifyAll(); pythonPathUpdater.verifyAll(); }); - test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { + test('Update entire workspace settings when there is more than one workspace folder and `Select at workspace level` is selected', async () => { workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); const expectedItems = [ + { label: Common.clearAll }, { label: 'one', description: path.dirname(folder1.uri.fsPath), - uri: folder1.uri + uri: folder1.uri, + detail: 'pythonPath', }, { label: 'two', description: path.dirname(folder2.uri.fsPath), - uri: folder2.uri + uri: folder2.uri, + detail: 'pythonPath', }, { - label: Interpreters.entireWorkspace(), - uri: folder1.uri - } + label: Interpreters.clearAtWorkspace, + uri: folder1.uri, + }, ]; appShell .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ - label: Interpreters.entireWorkspace(), - uri: folder1.uri - }) + label: Interpreters.clearAtWorkspace, + uri: folder1.uri, + }), ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater @@ -159,8 +183,8 @@ suite('Reset Interpreter Command', () => { TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.Workspace), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder1.uri) - ) + TypeMoq.It.isValue(folder1.uri), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -171,24 +195,97 @@ suite('Reset Interpreter Command', () => { workspace.verifyAll(); pythonPathUpdater.verifyAll(); }); + test('Update all folders and workspace scope if `Clear all` is selected', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { label: Common.clearAll }, + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri, + detail: 'pythonPath', + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri, + detail: 'pythonPath', + }, + { + label: Interpreters.clearAtWorkspace, + uri: folder1.uri, + }, + ]; + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: Common.clearAll, + 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()); + 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()); + + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + 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: Common.clearAll }, { label: 'one', description: path.dirname(folder1.uri.fsPath), - uri: folder1.uri + uri: folder1.uri, + detail: 'pythonPath', }, { label: 'two', description: path.dirname(folder2.uri.fsPath), - uri: folder2.uri + uri: folder2.uri, + detail: 'pythonPath', }, { - label: Interpreters.entireWorkspace(), - uri: folder1.uri - } + label: Interpreters.clearAtWorkspace, + uri: folder1.uri, + }, ]; appShell @@ -197,7 +294,7 @@ suite('Reset Interpreter Command', () => { .verifiable(TypeMoq.Times.once()); pythonPathUpdater .setup((p) => - p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 64928d8fe24d..7837245ec9d2 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -6,24 +6,52 @@ 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 { + ConfigurationTarget, + OpenDialogOptions, + QuickPick, + QuickPickItem, + QuickPickItemKind, + Uri, + WorkspaceFolder, +} from 'vscode'; +import { cloneDeep } from 'lodash'; +import { anything, instance, mock, when, verify } from 'ts-mockito'; 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 { Common, InterpreterQuickPickList, Interpreters } from '../../../../client/common/utils/localize'; +import { + IMultiStepInput, + IMultiStepInputFactory, + InputStep, + IQuickPickParameters, +} from '../../../../client/common/utils/multiStepInput'; import { + EnvGroups, InterpreterStateArgs, - SetInterpreterCommand + QuickPickType, + SetInterpreterCommand, } from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; import { IInterpreterQuickPickItem, IInterpreterSelector, - IPythonPathUpdaterServiceManager + IPythonPathUpdaterServiceManager, } from '../../../../client/interpreter/configuration/types'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { EventName } from '../../../../client/telemetry/constants'; +import * as Telemetry from '../../../../client/telemetry'; +import { MockWorkspaceConfiguration } from '../../../mocks/mockWorkspaceConfig'; +import { Commands, Octicons } from '../../../../client/common/constants'; +import { IInterpreterService, PythonEnvironmentsChangedEvent } from '../../../../client/interpreter/contracts'; +import { createDeferred, sleep } from '../../../../client/common/utils/async'; +import { SystemVariables } from '../../../../client/common/variables/systemVariables'; +import { untildify } from '../../../../client/common/helpers'; +import * as extapi from '../../../../client/envExt/api.internal'; + +type TelemetryEventType = { eventName: EventName; properties: unknown }; -// tslint:disable-next-line:max-func-body-length suite('Set Interpreter Command', () => { let workspace: TypeMoq.IMock<IWorkspaceService>; let interpreterSelector: TypeMoq.IMock<IInterpreterSelector>; @@ -34,12 +62,17 @@ suite('Set Interpreter Command', () => { let pythonSettings: TypeMoq.IMock<IPythonSettings>; let platformService: TypeMoq.IMock<IPlatformService>; let multiStepInputFactory: TypeMoq.IMock<IMultiStepInputFactory>; + let interpreterService: IInterpreterService; + let useEnvExtensionStub: sinon.SinonStub; const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; let setInterpreterCommand: SetInterpreterCommand; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + interpreterSelector = TypeMoq.Mock.ofType<IInterpreterSelector>(); multiStepInputFactory = TypeMoq.Mock.ofType<IMultiStepInputFactory>(); platformService = TypeMoq.Mock.ofType<IPlatformService>(); @@ -50,6 +83,11 @@ suite('Set Interpreter Command', () => { pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + interpreterService = mock<IInterpreterService>(); + when(interpreterService.refreshPromise).thenReturn(undefined); + when(interpreterService.triggerRefresh(anything(), anything())).thenResolve(); + workspace.setup((w) => w.rootPath).returns(() => 'rootPath'); + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); setInterpreterCommand = new SetInterpreterCommand( @@ -61,7 +99,8 @@ suite('Set Interpreter Command', () => { multiStepInputFactory.object, platformService.object, interpreterSelector.object, - workspace.object + workspace.object, + instance(interpreterService), ); }); @@ -70,32 +109,103 @@ suite('Set Interpreter Command', () => { }); suite('Test method _pickInterpreter()', async () => { - // tslint:disable-next-line: no-any - let _enterOrBrowseInterpreterPath: sinon.SinonStub<any>; + let _enterOrBrowseInterpreterPath: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let telemetryEvent: TelemetryEventType | undefined; + + const interpreterPath = 'path/to/interpreter'; const item: IInterpreterQuickPickItem = { - description: '', + description: interpreterPath, detail: '', - label: '', - path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any + label: 'This is the selected Python path', + path: `path/to/envFolder`, + interpreter: { + path: interpreterPath, + id: interpreterPath, + envType: EnvironmentType.Conda, + envPath: `path/to/envFolder`, + } as PythonEnvironment, + }; + const defaultInterpreterPath = 'defaultInterpreterPath'; + const defaultInterpreterPathSuggestion = { + label: `${Octicons.Gear} ${InterpreterQuickPickList.defaultInterpreterPath.label}`, + description: defaultInterpreterPath, + path: defaultInterpreterPath, + alwaysShow: true, + }; + + const noPythonInstalled = { + label: `${Octicons.Error} ${InterpreterQuickPickList.noPythonInstalled}`, + detail: InterpreterQuickPickList.clickForInstructions, + alwaysShow: true, + }; + + const tipToReloadWindow = { + label: `${Octicons.Lightbulb} Reload the window if you installed Python but don't see it`, + detail: `Click to run \`Developer: Reload Window\` command`, + alwaysShow: true, + }; + + const refreshedItem: IInterpreterQuickPickItem = { + description: interpreterPath, + detail: '', + label: 'Refreshed path', + path: `path/to/envFolder`, + interpreter: { + path: interpreterPath, + id: interpreterPath, + envType: EnvironmentType.Conda, + envPath: `path/to/envFolder`, + } as PythonEnvironment, }; const expectedEnterInterpreterPathSuggestion = { - label: InterpreterQuickPickList.enterPath.label(), - detail: InterpreterQuickPickList.enterPath.detail(), - alwaysShow: true + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, + alwaysShow: true, + }; + const expectedCreateEnvSuggestion = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, + alwaysShow: true, }; const currentPythonPath = 'python'; + const workspacePath = 'path/to/workspace'; + setup(() => { _enterOrBrowseInterpreterPath = sinon.stub( SetInterpreterCommand.prototype, - '_enterOrBrowseInterpreterPath' + '_enterOrBrowseInterpreterPath', ); _enterOrBrowseInterpreterPath.resolves(); + sendTelemetryStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: EventName, _, properties: unknown) => { + telemetryEvent = { + eventName, + properties, + }; + }); interpreterSelector - .setup((i) => i.getSuggestions(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([item])); + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => [item]); + interpreterSelector + .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => item); + pythonSettings.setup((p) => p.pythonPath).returns(() => currentPythonPath); + pythonSettings.setup((p) => p.defaultInterpreterPath).returns(() => defaultInterpreterPath); + + workspace + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isAny())) + .returns( + () => + new MockWorkspaceConfiguration({ + defaultInterpreterPath, + }), + ); + + workspace + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => (({ uri: { fsPath: workspacePath } } as unknown) as WorkspaceFolder)); + setInterpreterCommand = new SetInterpreterCommand( appShell.object, new PathUtils(false), @@ -105,11 +215,14 @@ suite('Set Interpreter Command', () => { multiStepInputFactory.object, platformService.object, interpreterSelector.object, - workspace.object + workspace.object, + instance(interpreterService), ); }); teardown(() => { + telemetryEvent = undefined; sinon.restore(); + Telemetry._resetSharedProperties(); }); test('Existing state path must be removed before displaying picker', async () => { @@ -117,8 +230,7 @@ suite('Set Interpreter Command', () => { 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)); + .returns(() => Promise.resolve(undefined as unknown)); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); @@ -128,53 +240,747 @@ suite('Set Interpreter Command', () => { 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), + const recommended = cloneDeep(item); + recommended.label = item.label; + recommended.description = interpreterPath; + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + ]; + const expectedParameters: IQuickPickParameters<QuickPickItem> = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions, - activeItem: item, matchOnDetail: true, - matchOnDescription: true + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, }; + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; multiStepInput - .setup((i) => i.showQuickPick(expectedParameters)) - // tslint:disable-next-line: no-any - .returns(() => Promise.resolve(undefined as any)) + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert.ok(false, 'Not a function'); + } + delete actualParameters!.activeItem; + assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); + }); + + test('Picker should show create env when set in options', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const recommended = cloneDeep(item); + recommended.label = item.label; + recommended.description = interpreterPath; + const suggestions = [ + expectedCreateEnvSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + ]; + const expectedParameters: IQuickPickParameters<QuickPickItem> = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state, undefined, { + showCreateEnvironment: true, + }); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert.ok(false, 'Not a function'); + } + delete actualParameters!.activeItem; + assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); + }); + + test('Picker should be displayed with expected items if no interpreters are available', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + noPythonInstalled, + ]; + const expectedParameters: IQuickPickParameters<QuickPickItem> = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, // Verify suggestions + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => []); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, noPythonInstalled); + } else { + assert.ok(false, 'Not a function'); + } + delete actualParameters!.activeItem; + assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); + }); + + test('Picker should install python if corresponding item is selected', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .returns(() => Promise.resolve((noPythonInstalled as unknown) as QuickPickItem)); + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => []); + commandManager + .setup((c) => c.executeCommand(Commands.InstallPython)) + .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); - multiStepInput.verifyAll(); + commandManager.verifyAll(); }); - test('If an item is selected, update state and return', async () => { + test('Picker should reload window if corresponding item is selected', 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)); + .returns(() => Promise.resolve((tipToReloadWindow as unknown) as QuickPickItem)); + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => []); + commandManager + .setup((c) => c.executeCommand('workbench.action.reloadWindow')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); - expect(state.path).to.equal(item.path, ''); + commandManager.verifyAll(); }); - test('If `Enter or browse...` option is selected, call the corresponding method with correct arguments', async () => { + test('Items displayed should be grouped if no refresh is going on', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const interpreterItems: IInterpreterQuickPickItem[] = [ + { + description: `${workspacePath}/interpreterPath1`, + detail: '', + label: 'This is the selected Python path', + path: `${workspacePath}/interpreterPath1`, + interpreter: { + id: `${workspacePath}/interpreterPath1`, + path: `${workspacePath}/interpreterPath1`, + envType: EnvironmentType.Venv, + } as PythonEnvironment, + }, + { + description: 'interpreterPath2', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath2', + interpreter: { + id: 'interpreterPath2', + path: 'interpreterPath2', + envType: EnvironmentType.VirtualEnvWrapper, + } as PythonEnvironment, + }, + { + description: 'interpreterPath3', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath3', + interpreter: { + id: 'interpreterPath3', + path: 'interpreterPath3', + envType: EnvironmentType.VirtualEnvWrapper, + } as PythonEnvironment, + }, + { + description: 'interpreterPath4', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath4', + interpreter: { + path: 'interpreterPath4', + id: 'interpreterPath4', + envType: EnvironmentType.Conda, + } as PythonEnvironment, + }, + item, + { + description: 'interpreterPath5', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath5', + interpreter: { + path: 'interpreterPath5', + id: 'interpreterPath5', + envType: EnvironmentType.Global, + } as PythonEnvironment, + }, + ]; + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => interpreterItems); + interpreterSelector + .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => item); + const recommended = cloneDeep(item); + recommended.label = item.label; + recommended.description = interpreterPath; + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + { label: EnvGroups.Workspace, kind: QuickPickItemKind.Separator }, + interpreterItems[0], + { label: EnvGroups.VirtualEnvWrapper, kind: QuickPickItemKind.Separator }, + interpreterItems[1], + interpreterItems[2], + { label: EnvGroups.Conda, kind: QuickPickItemKind.Separator }, + interpreterItems[3], + item, + { label: EnvGroups.Global, kind: QuickPickItemKind.Separator }, + interpreterItems[5], + ]; + const expectedParameters: IQuickPickParameters<QuickPickItem> = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + activeItem: recommended, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + assert.deepStrictEqual(actualParameters?.items, expectedParameters.items, 'Params not equal'); + }); + + test('Items displayed should be filtered out if a filter is provided', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const interpreterItems: IInterpreterQuickPickItem[] = [ + { + description: `${workspacePath}/interpreterPath1`, + detail: '', + label: 'This is the selected Python path', + path: `${workspacePath}/interpreterPath1`, + interpreter: { + id: `${workspacePath}/interpreterPath1`, + path: `${workspacePath}/interpreterPath1`, + envType: EnvironmentType.Venv, + } as PythonEnvironment, + }, + { + description: 'interpreterPath2', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath2', + interpreter: { + id: 'interpreterPath2', + path: 'interpreterPath2', + envType: EnvironmentType.VirtualEnvWrapper, + } as PythonEnvironment, + }, + { + description: 'interpreterPath3', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath3', + interpreter: { + id: 'interpreterPath3', + path: 'interpreterPath3', + envType: EnvironmentType.VirtualEnvWrapper, + } as PythonEnvironment, + }, + { + description: 'interpreterPath4', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath4', + interpreter: { + path: 'interpreterPath4', + id: 'interpreterPath4', + envType: EnvironmentType.Conda, + } as PythonEnvironment, + }, + item, + { + description: 'interpreterPath5', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath5', + interpreter: { + path: 'interpreterPath5', + id: 'interpreterPath5', + envType: EnvironmentType.Global, + } as PythonEnvironment, + }, + ]; + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => interpreterItems); + interpreterSelector + .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => item); + const recommended = cloneDeep(item); + recommended.label = item.label; + recommended.description = interpreterPath; + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + { label: EnvGroups.VirtualEnvWrapper, kind: QuickPickItemKind.Separator }, + interpreterItems[1], + interpreterItems[2], + { label: EnvGroups.Global, kind: QuickPickItemKind.Separator }, + interpreterItems[5], + ]; + const expectedParameters: IQuickPickParameters<QuickPickItem> = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + activeItem: recommended, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter( + multiStepInput.object, + state, + (e) => e.envType === EnvironmentType.VirtualEnvWrapper || e.envType === EnvironmentType.Global, + ); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + assert.deepStrictEqual(actualParameters?.items, expectedParameters.items, 'Params not equal'); + }); + + test('If system variables are used in the default interpreter path, make sure they are resolved when the path is displayed', async () => { + // Create a SetInterpreterCommand instance from scratch, and use a different defaultInterpreterPath from the rest of the tests. + const workspaceDefaultInterpreterPath = '${workspaceFolder}/defaultInterpreterPath'; + + const systemVariables = new SystemVariables(undefined, undefined, workspace.object); + const pathUtils = new PathUtils(false); + + const expandedPath = systemVariables.resolveAny(workspaceDefaultInterpreterPath); + const expandedDetail = pathUtils.getDisplayName(expandedPath); + + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + + pythonSettings.setup((p) => p.pythonPath).returns(() => currentPythonPath); + pythonSettings.setup((p) => p.defaultInterpreterPath).returns(() => workspaceDefaultInterpreterPath); + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + workspace.setup((w) => w.rootPath).returns(() => 'rootPath'); + workspace + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isAny())) + .returns( + () => + new MockWorkspaceConfiguration({ + defaultInterpreterPath: workspaceDefaultInterpreterPath, + }), + ); + + setInterpreterCommand = new SetInterpreterCommand( + appShell.object, + pathUtils, + pythonPathUpdater.object, + configurationService.object, + commandManager.object, + multiStepInputFactory.object, + platformService.object, + interpreterSelector.object, + workspace.object, + instance(interpreterService), + ); + + // Test info + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const recommended = cloneDeep(item); + recommended.label = item.label; + recommended.description = interpreterPath; + const separator = { label: EnvGroups.Recommended, kind: QuickPickItemKind.Separator }; + + const defaultPathSuggestion = { + label: `${Octicons.Gear} ${InterpreterQuickPickList.defaultInterpreterPath.label}`, + description: expandedDetail, + path: expandedPath, + alwaysShow: true, + }; + + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultPathSuggestion, + separator, + recommended, + ]; + const expectedParameters: IQuickPickParameters<QuickPickItem> = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert.ok(false, 'Not a function'); + } + delete actualParameters!.activeItem; + + assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); + }); + + test('Ensure a refresh is triggered if refresh button is clicked', async () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; multiStepInput .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - // tslint:disable-next-line: no-any - .returns(() => Promise.resolve(expectedEnterInterpreterPathSuggestion as any)); + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + expect(refreshButtons?.length).to.equal(1); + + await refreshButtons![0].callback!({} as QuickPick<QuickPickItem>); // Invoke callback, meaning that the refresh button is clicked. + + verify(interpreterService.triggerRefresh(anything(), anything())).once(); + }); + + test('Events to update quickpick updates the quickpick accordingly', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + let actualParameters: IQuickPickParameters<QuickPickItem> | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + const refreshPromiseDeferred = createDeferred(); + // Assume a refresh is currently going on... + when(interpreterService.refreshPromise).thenReturn(refreshPromiseDeferred.promise); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const onChangedCallback = actualParameters!.onChangeItem?.callback; + expect(onChangedCallback).to.not.equal(undefined, 'Callback not set'); + multiStepInput.verifyAll(); + + const separator = { label: EnvGroups.Conda, kind: QuickPickItemKind.Separator }; + const quickPick = { + items: [expectedEnterInterpreterPathSuggestion, defaultInterpreterPathSuggestion, separator, item], + activeItems: [item], + busy: false, + }; + interpreterSelector + .setup((i) => i.suggestionToQuickPickItem(TypeMoq.It.isAny(), undefined, false)) + .returns(() => refreshedItem); + + const changeEvent: PythonEnvironmentsChangedEvent = { + old: item.interpreter, + new: refreshedItem.interpreter, + }; + await onChangedCallback!(changeEvent, (quickPick as unknown) as QuickPick<QuickPickItem>); // Invoke callback, meaning that the items are supposed to change. + + assert.deepStrictEqual( + quickPick, + { + items: [ + expectedEnterInterpreterPathSuggestion, + defaultInterpreterPathSuggestion, + separator, + refreshedItem, + ], + activeItems: [refreshedItem], + busy: true, + }, + 'Quickpick not updated correctly', + ); + + // Refresh is over; set the final states accordingly + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => [refreshedItem]); + interpreterSelector + .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => refreshedItem); + interpreterSelector + .setup((i) => + i.suggestionToQuickPickItem(TypeMoq.It.isValue(refreshedItem.interpreter), undefined, false), + ) + .returns(() => refreshedItem); + when(interpreterService.refreshPromise).thenReturn(undefined); + + refreshPromiseDeferred.resolve(); + await sleep(1); + + const recommended = cloneDeep(refreshedItem); + recommended.label = refreshedItem.label; + recommended.description = `${interpreterPath} - ${Common.recommended}`; + assert.deepStrictEqual( + quickPick, + { + // Refresh has finished, so recommend an interpreter + items: [ + expectedEnterInterpreterPathSuggestion, + defaultInterpreterPathSuggestion, + separator, + recommended, + ], + activeItems: [recommended], + // Refresh has finished, so quickpick busy indicator should go away + busy: false, + }, + 'Quickpick not updated correctly after refresh has finished', + ); + + const newItem = { + description: `${workspacePath}/interpreterPath1`, + detail: '', + label: 'This is the selected Python path', + path: `${workspacePath}/interpreterPath1`, + interpreter: { + id: `${workspacePath}/interpreterPath1`, + path: `${workspacePath}/interpreterPath1`, + envType: EnvironmentType.Venv, + } as PythonEnvironment, + }; + const changeEvent2: PythonEnvironmentsChangedEvent = { + old: undefined, + new: newItem.interpreter, + }; + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => [refreshedItem, newItem]); + interpreterSelector + .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => refreshedItem); + interpreterSelector + .setup((i) => + i.suggestionToQuickPickItem(TypeMoq.It.isValue(refreshedItem.interpreter), undefined, false), + ) + .returns(() => refreshedItem); + interpreterSelector + .setup((i) => i.suggestionToQuickPickItem(TypeMoq.It.isValue(newItem.interpreter), undefined, false)) + .returns(() => newItem); + await onChangedCallback!(changeEvent2, (quickPick as unknown) as QuickPick<QuickPickItem>); // Invoke callback, meaning that the items are supposed to change. + + assert.deepStrictEqual( + quickPick, + { + items: [ + expectedEnterInterpreterPathSuggestion, + defaultInterpreterPathSuggestion, + separator, + recommended, + { label: EnvGroups.Workspace, kind: QuickPickItemKind.Separator }, + newItem, + ], + activeItems: [recommended], + busy: false, + }, + 'Quickpick not updated correctly', + ); + }); + + 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())).returns(() => Promise.resolve(item)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(state.path).to.equal(item.interpreter.envPath, ''); + }); + + test('If an item is selected, send SELECT_INTERPRETER_SELECTED telemetry with the "selected" property value', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(item)); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); - assert( + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepStrictEqual(telemetryEvent, { + eventName: EventName.SELECT_INTERPRETER_SELECTED, + properties: { action: 'selected' }, + }); + }); + + test('If the dropdown is dismissed, send SELECT_INTERPRETER_SELECTED telemetry with the "escape" property value', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepStrictEqual(telemetryEvent, { + eventName: EventName.SELECT_INTERPRETER_SELECTED, + properties: { action: 'escape' }, + }); + }); + + 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())) + .returns(() => Promise.resolve(expectedEnterInterpreterPathSuggestion)); + + const step = await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await step!(multiStepInput.object as any, state); + assert.ok( _enterOrBrowseInterpreterPath.calledOnceWith(multiStepInput.object, { path: undefined, - workspace: undefined - }) + workspace: undefined, + }), ); }); }); @@ -182,23 +988,27 @@ suite('Set Interpreter Command', () => { suite('Test method _enterOrBrowseInterpreterPath()', async () => { const items: QuickPickItem[] = [ { - label: InterpreterQuickPickList.browsePath.label(), - detail: InterpreterQuickPickList.browsePath.detail() - } + label: InterpreterQuickPickList.browsePath.label, + detail: InterpreterQuickPickList.browsePath.detail, + }, ]; const expectedParameters = { - placeholder: InterpreterQuickPickList.enterPath.placeholder(), + placeholder: InterpreterQuickPickList.enterPath.placeholder, items, - acceptFilterBoxTextAsSelection: true + acceptFilterBoxTextAsSelection: true, }; + let getItemsStub: sinon.SinonStub; + setup(() => { + getItemsStub = sinon.stub(SetInterpreterCommand.prototype, '_getItems').returns([]); + }); + teardown(() => sinon.restore()); 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)) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)) .verifiable(TypeMoq.Times.once()); await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); @@ -211,8 +1021,7 @@ suite('Set Interpreter Command', () => { 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)); + .returns(() => Promise.resolve('enteredPath')); await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); @@ -223,10 +1032,7 @@ suite('Set Interpreter Command', () => { 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)); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); appShell .setup((a) => a.showOpenDialog(TypeMoq.It.isAny())) .returns(() => Promise.resolve([expectedPathUri])); @@ -243,22 +1049,19 @@ suite('Set Interpreter Command', () => { filtersObject[filtersKey] = ['exe']; const expectedParams = { filters: filtersObject, - openLabel: InterpreterQuickPickList.browsePath.openButtonLabel(), + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, - title: InterpreterQuickPickList.browsePath.title() + title: InterpreterQuickPickList.browsePath.title, + defaultUri: 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(items[0] as any)); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); appShell - // tslint:disable-next-line: no-any - .setup((a) => a.showOpenDialog(expectedParams as any)) + .setup((a) => a.showOpenDialog(expectedParams as OpenDialogOptions)) .verifiable(TypeMoq.Times.once()); platformService.setup((p) => p.isWindows).returns(() => true); - await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state).ignoreErrors(); appShell.verifyAll(); }); @@ -268,23 +1071,173 @@ suite('Set Interpreter Command', () => { const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); const expectedParams = { filters: undefined, - openLabel: InterpreterQuickPickList.browsePath.openButtonLabel(), + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, - title: InterpreterQuickPickList.browsePath.title() + title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, }; - multiStepInput - .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - // tslint:disable-next-line: no-any - .returns(() => Promise.resolve(items[0] as any)); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); platformService.setup((p) => p.isWindows).returns(() => false); - await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state).ignoreErrors(); + + appShell.verifyAll(); + }); + + test('If `Browse...` option is selected with workspace, file browser opens at workspace root', async () => { + const workspaceUri = Uri.parse('file:///workspace/root'); + const state: InterpreterStateArgs = { path: undefined, workspace: workspaceUri }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const expectedParams = { + filters: undefined, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title, + defaultUri: workspaceUri, + }; + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); + appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); + platformService.setup((p) => p.isWindows).returns(() => false); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state).ignoreErrors(); appShell.verifyAll(); }); + + suite('SELECT_INTERPRETER_ENTERED_EXISTS telemetry', async () => { + let sendTelemetryStub: sinon.SinonStub; + let telemetryEvents: TelemetryEventType[] = []; + + setup(() => { + sendTelemetryStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: EventName, _, properties: unknown) => { + telemetryEvents.push({ + eventName, + properties, + }); + }); + }); + + teardown(() => { + telemetryEvents = []; + sinon.restore(); + Telemetry._resetSharedProperties(); + }); + + test('A telemetry event should be sent after manual entry of an intepreter path', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .returns(() => Promise.resolve('enteredPath')); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + const existsTelemetry = telemetryEvents[1]; + + sinon.assert.callCount(sendTelemetryStub, 2); + expect(existsTelemetry.eventName).to.equal(EventName.SELECT_INTERPRETER_ENTERED_EXISTS); + }); + + test('A telemetry event should be sent after browsing for an interpreter', 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, + defaultUri: undefined, + }; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(items[0])); + appShell + .setup((a) => a.showOpenDialog(expectedParams)) + .returns(() => Promise.resolve([{ fsPath: 'browsedPath' } as Uri])); + platformService.setup((p) => p.isWindows).returns(() => false); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + const existsTelemetry = telemetryEvents[1]; + + sinon.assert.callCount(sendTelemetryStub, 2); + expect(existsTelemetry.eventName).to.equal(EventName.SELECT_INTERPRETER_ENTERED_EXISTS); + }); + + enum SelectionPathType { + Absolute = 'absolute', + HomeRelative = 'home relative', + WorkspaceRelative = 'workspace relative', + } + type DiscoveredPropertyTestValues = { discovered: boolean; pathType: SelectionPathType }; + const discoveredPropertyTestMatrix: DiscoveredPropertyTestValues[] = [ + { discovered: true, pathType: SelectionPathType.Absolute }, + { discovered: true, pathType: SelectionPathType.HomeRelative }, + { discovered: true, pathType: SelectionPathType.WorkspaceRelative }, + { discovered: false, pathType: SelectionPathType.Absolute }, + { discovered: false, pathType: SelectionPathType.HomeRelative }, + { discovered: false, pathType: SelectionPathType.WorkspaceRelative }, + ]; + + const testDiscovered = async ( + discovered: boolean, + pathType: SelectionPathType, + ): Promise<TelemetryEventType> => { + let interpreterPath = ''; + let expandedPath = ''; + switch (pathType) { + case SelectionPathType.Absolute: { + interpreterPath = path.resolve(path.join('is', 'absolute', 'path')); + expandedPath = interpreterPath; + break; + } + case SelectionPathType.HomeRelative: { + interpreterPath = path.join('~', 'relative', 'path'); + expandedPath = untildify(interpreterPath); + break; + } + case SelectionPathType.WorkspaceRelative: + default: { + interpreterPath = path.join('..', 'workspace', 'path'); + expandedPath = path.normalize(path.resolve(interpreterPath)); + } + } + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(interpreterPath)); + + const suggestions = [ + { interpreter: { path: 'path/to/an/interpreter/' } }, + { interpreter: { path: '~/path/to/another/interpreter' } }, + { interpreter: { path: './.myvenv/bin/python' } }, + ] as IInterpreterQuickPickItem[]; + + if (discovered) { + suggestions.push({ interpreter: { path: expandedPath } } as IInterpreterQuickPickItem); + } + getItemsStub.restore(); + getItemsStub = sinon.stub(SetInterpreterCommand.prototype, '_getItems').returns(suggestions); + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + return telemetryEvents[1]; + }; + + for (const testValue of discoveredPropertyTestMatrix) { + test(`A telemetry event should be sent with the discovered prop set to ${ + testValue.discovered + } if the interpreter had ${ + testValue.discovered ? 'already' : 'not' + } been discovered, with an interpreter path path that is ${testValue.pathType})`, async () => { + const telemetryResult = await testDiscovered(testValue.discovered, testValue.pathType); + + expect(telemetryResult.properties).to.deep.equal({ discovered: testValue.discovered }); + }); + } + }); }); - // 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'); @@ -293,32 +1246,28 @@ suite('Set Interpreter Command', () => { detail: '', label: '', path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any + + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => undefined); - interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); const multiStepInput = { - // tslint:disable-next-line: no-any - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); - } + }, }; - multiStepInputFactory - .setup((f) => f.create()) - // tslint:disable-next-line: no-any - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput<unknown>); pythonPathUpdater .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.Global), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(undefined) - ) + TypeMoq.It.isValue(undefined), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -336,26 +1285,22 @@ suite('Set Interpreter Command', () => { detail: '', label: '', path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any + + interpreter: {} as PythonEnvironment, }; 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([])); + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); const multiStepInput = { - // tslint:disable-next-line: no-any - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); - } + }, }; - multiStepInputFactory - .setup((f) => f.create()) - // tslint:disable-next-line: no-any - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput<unknown>); pythonPathUpdater .setup((p) => @@ -363,8 +1308,8 @@ suite('Set Interpreter Command', () => { TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder.uri) - ) + TypeMoq.It.isValue(folder.uri), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -381,8 +1326,8 @@ suite('Set Interpreter Command', () => { detail: '', label: '', path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any + + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); @@ -390,40 +1335,39 @@ suite('Set Interpreter Command', () => { { label: 'one', description: path.dirname(folder1.uri.fsPath), - uri: folder1.uri + uri: folder1.uri, + detail: 'python', }, { label: 'two', description: path.dirname(folder2.uri.fsPath), - uri: folder2.uri + uri: folder2.uri, + detail: 'python', }, { - label: Interpreters.entireWorkspace(), - uri: folder1.uri - } + label: Interpreters.entireWorkspace, + uri: folder1.uri, + }, ]; - interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); const multiStepInput = { - // tslint:disable-next-line: no-any - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); - } + }, }; - multiStepInputFactory - .setup((f) => f.create()) - // tslint:disable-next-line: no-any - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput<unknown>); 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 - }) + uri: folder2.uri, + detail: 'python', + }), ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater @@ -432,8 +1376,8 @@ suite('Set Interpreter Command', () => { TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder2.uri) - ) + TypeMoq.It.isValue(folder2.uri), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -444,15 +1388,15 @@ suite('Set Interpreter Command', () => { workspace.verifyAll(); pythonPathUpdater.verifyAll(); }); - test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { + test('Update entire workspace settings when there is more than one workspace folder and `Select at workspace level` 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 + + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); @@ -460,40 +1404,36 @@ suite('Set Interpreter Command', () => { { label: 'one', description: path.dirname(folder1.uri.fsPath), - uri: folder1.uri + uri: folder1.uri, + detail: 'python', }, { label: 'two', description: path.dirname(folder2.uri.fsPath), - uri: folder2.uri + uri: folder2.uri, + detail: 'python', }, { - label: Interpreters.entireWorkspace(), - uri: folder1.uri - } + label: Interpreters.entireWorkspace, + uri: folder1.uri, + }, ]; - interpreterSelector - .setup((i) => i.getSuggestions(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([selectedItem])); + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => [selectedItem]); const multiStepInput = { - // tslint:disable-next-line: no-any - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); - } + }, }; - multiStepInputFactory - .setup((f) => f.create()) - // tslint:disable-next-line: no-any - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput<unknown>); appShell .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ - label: Interpreters.entireWorkspace(), - uri: folder1.uri - }) + label: Interpreters.entireWorkspace, + uri: folder1.uri, + }), ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater @@ -502,8 +1442,8 @@ suite('Set Interpreter Command', () => { TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.Workspace), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder1.uri) - ) + TypeMoq.It.isValue(folder1.uri), + ), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -515,29 +1455,29 @@ suite('Set Interpreter Command', () => { pythonPathUpdater.verifyAll(); }); test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); 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()); + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); + multiStepInputFactory.setup((f) => f.create()).verifiable(TypeMoq.Times.never()); const expectedItems = [ { label: 'one', description: path.dirname(folder1.uri.fsPath), - uri: folder1.uri + uri: folder1.uri, + detail: 'python', }, { label: 'two', description: path.dirname(folder2.uri.fsPath), - uri: folder2.uri + uri: folder2.uri, + detail: 'python', }, { - label: Interpreters.entireWorkspace(), - uri: folder1.uri - } + label: Interpreters.entireWorkspace, + uri: folder1.uri, + }, ]; appShell @@ -546,7 +1486,7 @@ suite('Set Interpreter Command', () => { .verifiable(TypeMoq.Times.once()); pythonPathUpdater .setup((p) => - p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -569,42 +1509,40 @@ suite('Set Interpreter Command', () => { multiStepInputFactory.object, platformService.object, interpreterSelector.object, - workspace.object + workspace.object, + instance(interpreterService), ); - let inputStep!: Function; + type InputStepType = () => Promise<InputStep<unknown> | void>; + let inputStep!: InputStepType; 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 + + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => undefined); - interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); const multiStepInput = { - // tslint:disable-next-line: no-any - run: (inputStepArg: any, state: InterpreterStateArgs) => { + run: (inputStepArg: InputStepType, 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); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput<unknown>); pythonPathUpdater .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.Global), TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(undefined) - ) + TypeMoq.It.isValue(undefined), + ), ) .returns(() => Promise.resolve()); @@ -612,9 +1550,9 @@ suite('Set Interpreter Command', () => { expect(inputStep).to.not.equal(undefined, ''); - assert(pickInterpreter.notCalled); + assert.ok(pickInterpreter.notCalled); await inputStep(); - assert(pickInterpreter.calledOnce); + assert.ok(pickInterpreter.calledOnce); }); }); }); diff --git a/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts index ffe15121ac07..2ec20be66990 100644 --- a/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts +++ b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts @@ -1,61 +1,59 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// eslint-disable-next-line max-classes-per-file import * as assert from 'assert'; +import * as path from 'path'; 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 { EnvironmentTypeComparer } from '../../../client/interpreter/configuration/environmentTypeComparer'; import { InterpreterSelector } from '../../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; import { IInterpreterComparer, IInterpreterQuickPickItem } from '../../../client/interpreter/configuration/types'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; +import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { getOSType, OSType } from '../../common'; -const info: PythonInterpreter = { +const info: PythonEnvironment = { architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: '', - type: InterpreterType.Unknown, + envType: EnvironmentType.Unknown, version: new SemVer('1.0.0-alpha'), sysPrefix: '', - sysVersion: '' + 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) { + + public interpreter = ({} as unknown) as PythonEnvironment; + + constructor(l: string, p: string, d?: string) { this.path = p; this.label = l; + this.description = d ?? p; } } -// 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 }; - + let newComparer: TypeMoq.IMock<IInterpreterComparer>; class TestInterpreterSelector extends InterpreterSelector { - // tslint:disable-next-line:no-unnecessary-override - public async suggestionToQuickPickItem( - suggestion: PythonInterpreter, - workspaceUri?: Uri - ): Promise<IInterpreterQuickPickItem> { + public suggestionToQuickPickItem(suggestion: PythonEnvironment, workspaceUri?: Uri): IInterpreterQuickPickItem { return super.suggestionToQuickPickItem(suggestion, workspaceUri); } } @@ -63,54 +61,44 @@ suite('Interpreters - selector', () => { 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>(); + newComparer = 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) - ); + newComparer.setup((c) => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); + selector = new TestInterpreterSelector(interpreterService.object, newComparer.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) + newComparer.object, + new PathUtils(isWindows), ); - const initial: PythonInterpreter[] = [ - { displayName: '1', path: 'c:/path1/path1', type: InterpreterType.Unknown }, - { displayName: '2', path: 'c:/path1/path1', type: InterpreterType.Unknown }, - { displayName: '2', path: 'c:/path2/path2', type: InterpreterType.Unknown }, - { displayName: '2 (virtualenv)', path: 'c:/path2/path2', type: InterpreterType.VirtualEnv }, - { displayName: '3', path: 'c:/path2/path2', type: InterpreterType.Unknown }, - { displayName: '4', path: 'c:/path4/path4', type: InterpreterType.Conda } - ].map((item) => { - return { ...info, ...item }; - }); + 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 }, + { + displayName: '5', + path: 'c:/path5/path', + envPath: 'c:/path5/path/to/env', + envType: EnvironmentType.Conda, + }, + ].map((item) => ({ ...info, ...item })); interpreterService - .setup((x) => x.getInterpreters(TypeMoq.It.isAny(), { onSuggestion: true })) + .setup((x) => x.getAllInterpreters(TypeMoq.It.isAny())) .returns(() => new Promise((resolve) => resolve(initial))); - const actual = await selector.getSuggestions(undefined); + const actual = await selector.getAllSuggestions(undefined); const expected: InterpreterQuickPickItem[] = [ new InterpreterQuickPickItem('1', 'c:/path1/path1'), @@ -118,45 +106,107 @@ suite('Interpreters - selector', () => { 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') + new InterpreterQuickPickItem('4', 'c:/path4/path4'), + new InterpreterQuickPickItem('5', 'c:/path5/path/to/env', 'c:/path5/path/to/env'), ]; - assert.equal(actual.length, expected.length, 'Suggestion lengths are different.'); + assert.strictEqual(actual.length, expected.length, 'Suggestion lengths are different.'); for (let i = 0; i < expected.length; i += 1) { - assert.equal( + assert.strictEqual( actual[i].label, expected[i].label, - `Suggestion label is different at ${i}: exected '${expected[i].label}', found '${actual[i].label}'.` + `Suggestion label is different at ${i}: expected '${expected[i].label}', found '${actual[i].label}'.`, ); - assert.equal( + assert.strictEqual( actual[i].path, expected[i].path, - `Suggestion path is different at ${i}: exected '${expected[i].path}', found '${actual[i].path}'.` + `Suggestion path is different at ${i}: expected '${expected[i].path}', found '${actual[i].path}'.`, + ); + assert.strictEqual( + actual[i].description, + expected[i].description, + `Suggestion description is different at ${i}: expected '${expected[i].description}', found '${actual[i].description}'.`, ); } }); }); - 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; + test('Should sort environments with local ones first', async () => { + const workspacePath = path.join('path', 'to', 'workspace'); + + const environments: PythonEnvironment[] = [ + { + displayName: 'one', + envPath: path.join('path', 'to', 'another', 'workspace', '.venv'), + path: path.join('path', 'to', 'another', 'workspace', '.venv', 'bin', 'python'), + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + }, + { + displayName: 'two', + envPath: path.join(workspacePath, '.venv'), + path: path.join(workspacePath, '.venv', 'bin', 'python'), + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + }, + { + displayName: 'three', + path: path.join('a', 'global', 'env', 'python'), + envPath: path.join('a', 'global', 'env'), + envType: EnvironmentType.Global, + }, + { + displayName: 'four', + envPath: path.join('a', 'conda', 'environment'), + path: path.join('a', 'conda', 'environment'), + envName: 'conda-env', + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + }, + ].map((item) => ({ ...info, ...item })); + 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']); + .setup((x) => x.getAllInterpreters(TypeMoq.It.isAny())) + .returns(() => new Promise((resolve) => resolve(environments))); + + const interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(TypeMoq.It.isAny())) + .returns(() => ({ folderUri: { fsPath: workspacePath } } as WorkspacePythonPath)); + + const environmentTypeComparer = new EnvironmentTypeComparer(interpreterHelper.object); + + selector = new TestInterpreterSelector( + interpreterService.object, + environmentTypeComparer, + new PathUtils(getOSType() === OSType.Windows), + ); + + const result = await selector.getAllSuggestions(undefined); + + const expected: InterpreterQuickPickItem[] = [ + new InterpreterQuickPickItem('two', path.join(workspacePath, '.venv', 'bin', 'python')), + new InterpreterQuickPickItem( + 'one', + path.join('path', 'to', 'another', 'workspace', '.venv', 'bin', 'python'), + ), + new InterpreterQuickPickItem('four', path.join('a', 'conda', 'environment')), + new InterpreterQuickPickItem('three', path.join('a', 'global', 'env', 'python')), + ]; + + assert.strictEqual(result.length, expected.length, 'Suggestion lengths are different.'); + + for (let i = 0; i < expected.length; i += 1) { + assert.strictEqual( + result[i].label, + expected[i].label, + `Suggestion label is different at ${i}: expected '${expected[i].label}', found '${result[i].label}'.`, + ); + assert.strictEqual( + result[i].path, + expected[i].path, + `Suggestion path is different at ${i}: expected '${expected[i].path}', found '${result[i].path}'.`, + ); + } }); }); diff --git a/src/test/constants.ts b/src/test/constants.ts index 2e255ecec9d5..1f2d7b4909cf 100644 --- a/src/test/constants.ts +++ b/src/test/constants.ts @@ -6,7 +6,7 @@ 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_TIMEOUT = 60_000; 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'; @@ -21,7 +21,6 @@ function isMultitrootTest() { 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; @@ -38,5 +37,5 @@ export const SMOKE_TEST_EXTENSIONS_DIR = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'tmp', 'ext', - 'smokeTestExtensionsFolder' + 'smokeTestExtensionsFolder', ); diff --git a/src/test/core.ts b/src/test/core.ts index 8db9d06665ec..3308eecdb21d 100644 --- a/src/test/core.ts +++ b/src/test/core.ts @@ -9,7 +9,6 @@ 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 deleted file mode 100644 index 98998613cddd..000000000000 --- a/src/test/datascience/.vscode/settings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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", - "EnableTrustedNotebooks" - ], - // 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 deleted file mode 100644 index 02a53318cf00..000000000000 --- a/src/test/datascience/DefaultSalesReport.csv +++ /dev/null @@ -1,278 +0,0 @@ -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 deleted file mode 100644 index ef804c86082e..000000000000 --- a/src/test/datascience/activation.unit.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -// 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 { PythonInterpreter } 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<PythonInterpreter>; - let contextService: ActiveEditorContextService; - let fakeTimer: FakeClock; - const interpreter = createPythonInterpreter(); - - setup(async () => { - fakeTimer = new FakeClock(); - openedEventEmitter = new EventEmitter<INotebookEditor>(); - interpreterEventEmitter = new EventEmitter<PythonInterpreter>(); - 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 deleted file mode 100644 index 46dff8b23e26..000000000000 --- a/src/test/datascience/cellFactory.unit.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -// 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 deleted file mode 100644 index 8bdf6855314f..000000000000 --- a/src/test/datascience/cellMatcher.unit.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -// 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 deleted file mode 100644 index 95c12c218d9f..000000000000 --- a/src/test/datascience/color.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -// 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 deleted file mode 100644 index c6ee1f605ed0..000000000000 --- a/src/test/datascience/commands/commandRegistry.unit.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -// 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 deleted file mode 100644 index 8803ab4c0ab4..000000000000 --- a/src/test/datascience/commands/notebookCommands.functional.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import type { Kernel } from '@jupyterlab/services/lib/kernel/kernel'; -import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher'; -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 } 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 { InterpreterType } 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: '', - type: InterpreterType.Conda, - architecture: Architecture.Unknown, - sysPrefix: '', - sysVersion: '' - }; - const remoteSelections: IKernelSpecQuickPickItem[] = [ - { - label: 'foobar', - selection: { - kernelSpec: undefined, - kernelModel: remoteKernel, - interpreter: undefined - } - } - ]; - const localSelections: IKernelSpecQuickPickItem[] = [ - { - label: 'foobar', - selection: { - kernelSpec: localKernel, - kernelModel: undefined, - interpreter: undefined - } - }, - { - label: 'foobaz', - selection: { - kernelSpec: undefined, - kernelModel: undefined, - interpreter: selectedInterpreter - } - } - ]; - - [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 - ); - }); - - 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 createNotebookMock() { - const obj = mock(JupyterNotebookBase); - when((obj as any).then).thenReturn(undefined); - return obj; - } - 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 notebook', 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 }); - verify( - notebook.setKernelSpec( - argThat((o) => { - return isLocalConnection - ? o.name === localKernel.name - : o.name === remoteKernel.name && o.lastActivityTime; - }), - anything(), - anything() - ) - ).once(); - }); - 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)(); - - verify( - nativeEditor.setKernelSpec( - argThat((o) => { - return isLocalConnection - ? o.name === localKernel.name - : o.name === remoteKernel.name && o.lastActivityTime; - }), - anything(), - anything() - ) - ).once(); - }); - 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)(); - - verify( - interactiveWindow.setKernelSpec( - argThat((o) => { - return isLocalConnection - ? o.name === localKernel.name - : o.name === remoteKernel.name && o.lastActivityTime; - }), - anything(), - anything() - ) - ).once(); - }); - 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)(); - - verify( - nativeEditor.setKernelSpec( - argThat((o) => { - return isLocalConnection - ? o.name === localKernel.name - : o.name === remoteKernel.name && o.lastActivityTime; - }), - anything(), - anything() - ) - ).once(); - }); - 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 deleted file mode 100644 index 4cec791a08a4..000000000000 --- a/src/test/datascience/commands/serverSelector.unit.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index e9dff1eab730..000000000000 --- a/src/test/datascience/common.unit.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index 46c757c0f12f..000000000000 --- a/src/test/datascience/crossProcessLock.unit.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 5fce7b2ae4e9..000000000000 --- a/src/test/datascience/data-viewing/dataViewer.unit.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -// 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, IWebPanelProvider, IWorkspaceService } from '../../../client/common/application/types'; -import { WebPanelProvider } from '../../../client/common/application/webPanels/webPanelProvider'; -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: IWebPanelProvider; - 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(WebPanelProvider); - 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 deleted file mode 100644 index 1425eacc3865..000000000000 --- a/src/test/datascience/data-viewing/dataViewerPDependencyService.unit.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; - -suite('DataScience - DataViewerDependencyService', () => { - let dependencyService: DataViewerDependencyService; - let appShell: IApplicationShell; - let pythonExecFactory: IPythonExecutionFactory; - let installer: IInstaller; - let interpreter: PythonInterpreter; - let pythonExecService: IPythonExecutionService; - setup(async () => { - interpreter = { - architecture: Architecture.Unknown, - displayName: '', - path: path.join('users', 'python', 'bin', 'python.exe'), - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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 deleted file mode 100644 index 5e3443ca4a76..000000000000 --- a/src/test/datascience/dataScienceIocContainer.ts +++ /dev/null @@ -1,1570 +0,0 @@ -// 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, - IWebPanelOptions, - IWebPanelProvider, - IWorkspaceService -} from '../../client/common/application/types'; -import { WebPanelProvider } from '../../client/common/application/webPanels/webPanelProvider'; -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 { EnableTrustedNotebooks, 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 { 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 { 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter[] = []; - 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: PythonInterpreter = { - path: '/foo/bar/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - displayName: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 - }; - private workingPython2: PythonInterpreter = { - path: '/foo/baz/python.exe', - version: new SemVer('3.6.7-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - displayName: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 - }; - - private webPanelProvider = mock(WebPanelProvider); - 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<IWebPanelProvider>(IWebPanelProvider, WebBrowserPanelProvider); - this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, IPyWidgetScriptSource); - this.serviceManager.addSingleton<IHttpClient>(IHttpClient, HttpClient); - } else { - this.serviceManager.addSingletonInstance<IWebPanelProvider>( - IWebPanelProvider, - instance(this.webPanelProvider) - ); - } - 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); - when(experimentService.inExperiment(EnableTrustedNotebooks.experiment)).thenResolve(true); - 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.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); - - // 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<PythonInterpreter | undefined> { - const list = await this.getFunctionalTestInterpreters(); - return list ? list[0] : undefined; - } - - public async getFunctionalTestInterpreters(): Promise<PythonInterpreter[]> { - // 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 PythonInterpreter[]; - 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: PythonInterpreter, 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: IWebPanelOptions) { - 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: 60000, - 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: PythonInterpreter): Promise<boolean | undefined> { - try { - 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 - }); - return pythonProcess.isModuleInstalled('livelossplot'); // Should we check all dependencies? - } - } catch (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 deleted file mode 100644 index db20c2c28534..000000000000 --- a/src/test/datascience/datascience.unit.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -// 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 deleted file mode 100644 index fab0eb8d2f4a..000000000000 --- a/src/test/datascience/datascienceSurveyBanner.unit.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -// 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 deleted file mode 100644 index d10c18e81f6c..000000000000 --- a/src/test/datascience/dataviewer.functional.test.tsx +++ /dev/null @@ -1,312 +0,0 @@ -// 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 deleted file mode 100644 index f712d9d0f189..000000000000 --- a/src/test/datascience/debugLocationTracker.unit.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// 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 deleted file mode 100644 index 435fa3e01701..000000000000 --- a/src/test/datascience/debugger.functional.test.tsx +++ /dev/null @@ -1,448 +0,0 @@ -// 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 deleted file mode 100644 index d043a88ab276..000000000000 --- a/src/test/datascience/dsTestSetup.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 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 deleted file mode 100644 index d59c2f92ca9b..000000000000 --- a/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts +++ /dev/null @@ -1,559 +0,0 @@ -// 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 deleted file mode 100644 index 3733e518ea41..000000000000 --- a/src/test/datascience/editor-integration/codelensprovider.unit.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -// 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 deleted file mode 100644 index a632b46c14a4..000000000000 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ /dev/null @@ -1,2263 +0,0 @@ -// 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 deleted file mode 100644 index 13867648355b..000000000000 --- a/src/test/datascience/editor-integration/gotocell.functional.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -// 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 deleted file mode 100644 index b84c0a07f969..000000000000 --- a/src/test/datascience/editor-integration/helpers.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index ce686aaa5914..000000000000 --- a/src/test/datascience/errorHandler.functional.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index ab855ccee836..000000000000 --- a/src/test/datascience/errorHandler.unit.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -// 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 deleted file mode 100644 index 38f596342604..000000000000 --- a/src/test/datascience/execution.unit.test.ts +++ /dev/null @@ -1,1103 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - path: '/foo/bar/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 - }; - - const missingKernelPython: PythonInterpreter = { - path: '/foo/baz/python.exe', - version: new SemVer('3.1.1-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 - }; - - const missingNotebookPython: PythonInterpreter = { - path: '/bar/baz/python.exe', - version: new SemVer('2.1.1-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 - }; - - const missingNotebookPython2: PythonInterpreter = { - path: '/two/baz/python.exe', - version: new SemVer('2.1.1'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.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: PythonInterpreter, - notebookStdErr?: string[], - skipSearch?: boolean - ): JupyterExecutionFactory { - return createExecutionAndReturnProcessService(activeInterpreter, notebookStdErr, skipSearch) - .jupyterExecutionFactory; - } - function createExecutionAndReturnProcessService( - activeInterpreter: PythonInterpreter, - 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.getKernelForLocalConnection( - anything(), - anything(), - anything(), - anything(), - anything(), - anything() - ) - ).thenResolve({ - kernelSpec - }); - - const dependencyService = mock(JupyterInterpreterDependencyService); - when(dependencyService.areDependenciesInstalled(anything(), anything())).thenCall( - async (interpreter: PythonInterpreter) => { - if (interpreter === missingNotebookPython) { - return false; - } - return true; - } - ); - when(dependencyService.isExportSupported(anything(), anything())).thenCall( - async (interpreter: PythonInterpreter) => { - if (interpreter === missingNotebookPython) { - return false; - } - return true; - } - ); - when(dependencyService.getDependenciesNotInstalled(anything(), anything())).thenCall( - async (interpreter: PythonInterpreter) => { - 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 deleted file mode 100644 index 0d91a51a5850..000000000000 --- a/src/test/datascience/executionServiceMock.ts +++ /dev/null @@ -1,84 +0,0 @@ -// 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 deleted file mode 100644 index 90b01e936468..000000000000 --- a/src/test/datascience/export/exportFileOpener.unit.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -// 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 deleted file mode 100644 index 5ed02e7fd743..000000000000 --- a/src/test/datascience/export/exportManager.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -// 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 deleted file mode 100644 index d179aa49a714..000000000000 --- a/src/test/datascience/export/exportToHTML.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index 8845c6d890b4..000000000000 --- a/src/test/datascience/export/exportToPython.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index c59d133f3feb..000000000000 --- a/src/test/datascience/export/exportUtil.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index f9a7d41fbd1f..000000000000 --- a/src/test/datascience/export/test.ipynb +++ /dev/null @@ -1,39 +0,0 @@ -{ - "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 deleted file mode 100644 index 050d9ac5e97d..000000000000 --- a/src/test/datascience/export/testPDF.ipynb +++ /dev/null @@ -1,62 +0,0 @@ -{ - "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 deleted file mode 100644 index f660e395fe25..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.eslintrc.js +++ /dev/null @@ -1,20 +0,0 @@ -/**@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 deleted file mode 100644 index eaf5a2953c34..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index af515502dfd1..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - // 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 deleted file mode 100644 index e0a96d6afd7e..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/launch.json +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index c8b0c2086cfe..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscodeignore +++ /dev/null @@ -1,6 +0,0 @@ -.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 deleted file mode 100644 index a822212b683e..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100644 index df1dbfc9841c..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package-lock.json +++ /dev/null @@ -1,4657 +0,0 @@ -{ - "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 deleted file mode 100644 index da0796dd4018..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "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 deleted file mode 100644 index 645c62b26ea5..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/extension.ts +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index ddfc530dd761..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/serverPicker.ts +++ /dev/null @@ -1,99 +0,0 @@ -// 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 deleted file mode 100644 index f98ce2d07166..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/typings/python.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 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 deleted file mode 100644 index 287f63f07dfb..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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 deleted file mode 100644 index 423b78185bbb..000000000000 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/webpack.config.js +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 deleted file mode 100644 index 17da214da465..000000000000 --- a/src/test/datascience/foo.py +++ /dev/null @@ -1 +0,0 @@ -# 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 deleted file mode 100644 index e4a9791f3cc3..000000000000 --- a/src/test/datascience/helpers.ts +++ /dev/null @@ -1,63 +0,0 @@ -// 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 deleted file mode 100644 index a7919dca5d69..000000000000 --- a/src/test/datascience/inputHistory.unit.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -// 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 deleted file mode 100644 index daa822cb5adf..000000000000 --- a/src/test/datascience/intellisense.functional.test.tsx +++ /dev/null @@ -1,485 +0,0 @@ -// 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 deleted file mode 100644 index 8089af05eeb1..000000000000 --- a/src/test/datascience/intellisense.unit.test.ts +++ /dev/null @@ -1,619 +0,0 @@ -// 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 deleted file mode 100644 index 015dfccaedff..000000000000 --- a/src/test/datascience/interactive-common/notebookProvider.unit.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index 9b82f7bc64c8..000000000000 --- a/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - path: '/foo/bar/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.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 deleted file mode 100644 index cde9b602e5bf..000000000000 --- a/src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -// 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 { EnableTrustedNotebooks } from '../../../client/common/experiments/groups'; -import { IDisposable, IExperimentService } 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 experiments: IExperimentService; - 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 = []; - - experiments = mock<IExperimentService>(); - - when(trustService.trustNotebook(anything(), anything())).thenResolve(); - when(experiments.inExperiment(anything())).thenCall((exp) => - Promise.resolve(exp === EnableTrustedNotebooks.experiment) - ); - 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, - instance(experiments) - ); - - clock = fakeTimers.install(); - - contextKeySet = sinon.stub(ContextKey.prototype, 'set'); - contextKeySet.resolves(); - }); - teardown(() => { - sinon.restore(); - disposeAllDisposables(disposables); - clock.uninstall(); - }); - - test('Context not set if not in experiment', async () => { - when(experiments.inExperiment(anything())).thenResolve(false); - - await trustCommandHandler.activate(); - await clock.runAllAsync(); - - assert.equal(contextKeySet.callCount, 0); - }); - test('Context set if in experiment', async () => { - when(experiments.inExperiment(anything())).thenCall((exp) => - Promise.resolve(exp === EnableTrustedNotebooks.experiment) - ); - - await trustCommandHandler.activate(); - await clock.runAllAsync(); - - assert.equal(contextKeySet.callCount, 1); - }); - 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 deleted file mode 100644 index eb4f44cc8888..000000000000 --- a/src/test/datascience/interactive-common/trustService.unit.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -// 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 { ExperimentService } from '../../../client/common/experiments/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 experimentService = mock(ExperimentService); - 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)); - when(experimentService.inExperiment(anything())).thenResolve(true); // Pretend we're in an experiment so that trust checks proceed - - const digestStorage = new DigestStorage(instance(fileSystem), context.object); - trustService = new TrustService(instance(experimentService), 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 deleted file mode 100644 index 66f9ae014e33..000000000000 --- a/src/test/datascience/interactive-ipynb/nativeEditorProvider.functional.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -// 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 { concatMultilineStringInput } 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(concatMultilineStringInput(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 deleted file mode 100644 index e99c89fe15d7..000000000000 --- a/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts +++ /dev/null @@ -1,613 +0,0 @@ -// 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, - IWebPanelMessageListener, - IWebPanelProvider, - IWorkspaceService -} from '../../../client/common/application/types'; -import { WebPanel } from '../../../client/common/application/webPanels/webPanel'; -import { WebPanelProvider } from '../../../client/common/application/webPanels/webPanelProvider'; -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 { concatMultilineStringInput } 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: IWebPanelProvider; - 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(WebPanelProvider); - 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())).thenReturn(Promise.resolve()); - - testIndex += 1; - when(crypto.createHash(anything(), 'string')).thenReturn(`${testIndex}`); - - let listener: IWebPanelMessageListener; - const webPanel = mock(WebPanel); - 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, - trustService, - new NotebookModelFactory(false) - ); - - return new NotebookStorageProvider(notebookStorage, []); - } - - 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(concatMultilineStringInput(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(concatMultilineStringInput(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(concatMultilineStringInput(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 deleted file mode 100644 index d93d4846980a..000000000000 --- a/src/test/datascience/interactivePanel.functional.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -// 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 { 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 - }, - 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, - shouldShowTrustMessage: false - }; - }); - 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); - }); -}); diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx deleted file mode 100644 index 6fdb06faefdd..000000000000 --- a/src/test/datascience/interactiveWindow.functional.test.tsx +++ /dev/null @@ -1,1308 +0,0 @@ -// 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 { concatMultilineStringInput } 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 () => { - 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'); - }, - () => { - 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, concatMultilineStringInput(cells[0].data.source)); - - // Last cell should generate a series of updates. Verify we end up with a single image - await addCode(ioc, concatMultilineStringInput(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 deleted file mode 100644 index 377a0bb0300b..000000000000 --- a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -// 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 deleted file mode 100644 index cb472248a076..000000000000 --- a/src/test/datascience/interactiveWindowTestHelpers.tsx +++ /dev/null @@ -1,109 +0,0 @@ -// 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 deleted file mode 100644 index c43cb1b0fb46..000000000000 --- a/src/test/datascience/ipywidgets/cdnWidgetScriptSourceProvider.unit.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -// 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: '' - }; - 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 deleted file mode 100644 index d87ada1dd0e2..000000000000 --- a/src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index cfe9f30bb2c2..000000000000 --- a/src/test/datascience/ipywidgets/ipyWidgetScriptSourceProvider.unit.test.ts +++ /dev/null @@ -1,318 +0,0 @@ -// 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 { 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: '' - }; - 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 deleted file mode 100644 index 1bd377801159..000000000000 --- a/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -// 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 { 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 filesToLookSerachFor = `*${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.getKernelSpec()).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.getKernelSpec()).thenReturn({ - 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.getKernelSpec()).thenReturn({ - 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(filesToLookSerachFor, 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.getKernelSpec()).thenReturn({ path: kernelPath } 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(filesToLookSerachFor, searchDirectory)).once(); - }); - test('Ensure we cache the list of widgets source (when nothing is found)', async () => { - when(notebook.getKernelSpec()).thenReturn({ - 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.getKernelSpec()).thenReturn({ - 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(filesToLookSerachFor, 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.getKernelSpec()).thenReturn({ - 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(filesToLookSerachFor, 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.getKernelSpec()).thenReturn({ - 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(filesToLookSerachFor, searchDirectory)).once(); - }); -}); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts deleted file mode 100644 index f29cab813673..000000000000 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - path: '', - architecture: Architecture.Unknown, - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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 deleted file mode 100644 index 9b40580acacf..000000000000 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.unit.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index 55685e69afe1..000000000000 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -// 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 deleted file mode 100644 index 30b9e6acec0e..000000000000 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter | undefined; - let memento: Memento; - let interpreterSelectionState: JupyterInterpreterStateStore; - let oldVersionCacheStateStore: JupyterInterpreterOldCacheStateStore; - const selectedJupyterInterpreter = createPythonInterpreter({ displayName: 'JupyterInterpreter' }); - const pythonInterpreter: PythonInterpreter = { - path: 'some path', - architecture: Architecture.Unknown, - sysPrefix: '', - sysVersion: '', - type: InterpreterType.Unknown - }; - const secondPythonInterpreter: PythonInterpreter = { - path: 'second interpreter path', - architecture: Architecture.Unknown, - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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 deleted file mode 100644 index ef7ce4bfb9fe..000000000000 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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 { PythonInterpreter } 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<PythonInterpreter>; - - setup(() => { - memento = mock(MockMemento); - interpreterService = mock(JupyterInterpreterService); - when(memento.update(anything(), anything())).thenResolve(); - interpreterSelectedEventEmitter = new EventEmitter<PythonInterpreter>(); - 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 deleted file mode 100644 index a95b8b8f79a4..000000000000 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts +++ /dev/null @@ -1,432 +0,0 @@ -// 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 deleted file mode 100644 index 9c10b3080736..000000000000 --- a/src/test/datascience/jupyter/jupyterCellOutputMimeTypeTracker.unit.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -// 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 deleted file mode 100644 index 0ebc9e2e9c5d..000000000000 --- a/src/test/datascience/jupyter/jupyterConnection.unit.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -// 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 { 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, - // 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 deleted file mode 100644 index d4a2c858e478..000000000000 --- a/src/test/datascience/jupyter/jupyterSession.unit.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -// 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, deepEqual, 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 { LiveKernelModel } from '../../../client/datascience/jupyter/kernels/types'; -import { IJupyterConnection, IJupyterKernelSpec } from '../../../client/datascience/types'; -import { MockOutputChannel } from '../../mockClasses'; - -// tslint:disable: max-func-body-length -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: typemoq.IMock<IJupyterConnection>; - let serverSettings: typemoq.IMock<ServerConnection.ISettings>; - let kernelSpec: typemoq.IMock<IJupyterKernelSpec | LiveKernelModel>; - 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 = typemoq.Mock.ofType<IJupyterConnection>(); - serverSettings = typemoq.Mock.ofType<ServerConnection.ISettings>(); - kernelSpec = typemoq.Mock.ofType<IJupyterKernelSpec | LiveKernelModel>(); - session = mock(DefaultSession); - kernel = mock(DefaultKernel); - when(session.kernel).thenReturn(instance(kernel)); - statusChangedSignal = mock(Signal); - kernelChangedSignal = mock(Signal); - when(session.statusChanged).thenReturn(instance(statusChangedSignal)); - when(session.kernelChanged).thenReturn(instance(kernelChangedSignal)); - when(session.kernel).thenReturn(instance(kernel)); - when(kernel.status).thenReturn('idle'); - 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( - connection.object, - serverSettings.object, - kernelSpec.object, - instance(sessionManager), - instance(contentsManager), - channel, - () => { - restartSessionCreatedEvent.resolve(); - }, - () => { - restartSessionUsedEvent.resolve(); - } - ); - }); - - async function connect() { - const nbFile = 'file path'; - // tslint:disable-next-line: no-any - when(contentsManager.newUntitled(deepEqual({ type: 'notebook' }))).thenResolve({ path: nbFile } as any); - when(contentsManager.delete(anything())).thenResolve(); - when(sessionManager.startNew(anything())).thenResolve(instance(session)); - kernelSpec.setup((k) => k.name).returns(() => 'some name'); - kernelSpec.setup((k) => k.id).returns(() => undefined); - - 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 () => { - connection.setup((c) => c.localLaunch).returns(() => 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())).never(); - // 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 () => { - connection.setup((c) => c.localLaunch).returns(() => 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())).never(); - // 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 - }, - 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(newActiveRemoteKernel, 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())).twice(); - }); - 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); - restartSessionCreatedEvent = createDeferred(); - restartSessionUsedEvent = createDeferred(); - when(newSession.statusChanged).thenReturn(instance(newStatusChangedSignal)); - when(newSession.kernelChanged).thenReturn(instance(newKernelChangedSignal)); - // 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(newKernel, 10000); - - // Wait untill a new session has been started. - await newSessionCreated.promise; - // One original, one new session. - verify(sessionManager.startNew(anything())).thrice(); - }); - 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(); - connection.setup((c) => c.localLaunch).returns(() => 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 deleted file mode 100644 index 6002f48bcaa6..000000000000 --- a/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -// 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 deleted file mode 100644 index 4b360c04af6c..000000000000 --- a/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -// 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 { InterpreterType } 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: '', - type: InterpreterType.Conda, - displayName: 'Hello1' - }, - path: 'p1', - detail: '<user friendly path>', - description: '' - }, - { - label: 'Hello1', - interpreter: { - architecture: Architecture.Unknown, - path: 'p2', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.Conda, - displayName: 'Hello2' - }, - path: 'p1', - detail: '<user friendly path>', - description: '' - }, - { - label: 'Hello1', - interpreter: { - architecture: Architecture.Unknown, - path: 'p3', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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 - }, - kernelSpec: undefined - }, - 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 - }, - kernelSpec: undefined - }, - 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 } - }; - }); - const expectedInterpreterItems: IKernelSpecQuickPickItem[] = allInterpreters.map((item) => { - return { - ...item, - label: item.label, - detail: '<user friendly path>', - description: '', - selection: { kernelModel: undefined, interpreter: item.interpreter, kernelSpec: undefined } - }; - }); - 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 } - }; - }); - const expectedInterpreterItems: IKernelSpecQuickPickItem[] = allInterpreters.map((item) => { - return { - ...item, - label: item.label, - detail: '<user friendly path>', - description: '', - selection: { kernelModel: undefined, interpreter: item.interpreter, kernelSpec: undefined } - }; - }); - 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 deleted file mode 100644 index 3b6e694bc77f..000000000000 --- a/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts +++ /dev/null @@ -1,915 +0,0 @@ -// 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, 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 { InterpreterType, PythonInterpreter } from '../../../../client/pythonEnvironments/info'; - -// tslint:disable: max-func-body-length no-unused-expression - -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: PythonInterpreter = { - displayName: 'Something', - architecture: Architecture.Unknown, - path: 'somePath', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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.isEmpty(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.isEmpty(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 } - // tslint:disable-next-line: no-any - } as any); - - const kernel = await kernelSelector.selectRemoteKernel( - undefined, - new StopWatch(), - instance(sessionManager) - ); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isOk(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', - // tslint:disable-next-line: no-any - session: {} as any - }, - { - lastActivityTime: new Date(), - name: '2two', - numberOfConnections: 1, - id: 'id2', - display_name: '2', - // tslint:disable-next-line: no-any - session: {} as any - }, - { - lastActivityTime: new Date(), - name: '3three', - numberOfConnections: 1, - id: 'id3', - display_name: '3', - // tslint:disable-next-line: no-any - session: {} as any - }, - { - lastActivityTime: new Date(), - name: '4four', - numberOfConnections: 1, - id: 'id4', - display_name: '4', - // tslint:disable-next-line: no-any - session: {} as any - } - ]; - const quickPickItems: IKernelSpecQuickPickItem[] = kernelModels.map((kernelModel) => { - return { - label: '', - selection: { kernelModel, kernelSpec: undefined, interpreter: undefined } - }; - }); - - when( - kernelSelectionProvider.getKernelSelectionsForRemoteSession( - anything(), - instance(sessionManager), - anything() - ) - ).thenResolve(quickPickItems); - when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve(undefined); - - // tslint:disable-next-line: no-any - kernelSelector.addKernelToIgnoreList({ id: 'id2' } as any); - // tslint:disable-next-line: no-any - kernelSelector.addKernelToIgnoreList({ clientId: 'id4' } as any); - const kernel = await kernelSelector.selectRemoteKernel( - undefined, - new StopWatch(), - instance(sessionManager) - ); - - assert.isEmpty(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 || '')) - ); - }); - test('Should hide kernel from local sessions', async () => { - const kernelModels: LiveKernelModel[] = [ - { - lastActivityTime: new Date(), - name: '1one', - numberOfConnections: 1, - id: 'id1', - display_name: '1', - // tslint:disable-next-line: no-any - session: {} as any - }, - { - lastActivityTime: new Date(), - name: '2two', - numberOfConnections: 1, - id: 'id2', - display_name: '2', - // tslint:disable-next-line: no-any - session: {} as any - }, - { - lastActivityTime: new Date(), - name: '3three', - numberOfConnections: 1, - id: 'id3', - display_name: '3', - // tslint:disable-next-line: no-any - session: {} as any - }, - { - lastActivityTime: new Date(), - name: '4four', - numberOfConnections: 1, - id: 'id4', - display_name: '4', - // tslint:disable-next-line: no-any - session: {} as any - } - ]; - const quickPickItems: IKernelSpecQuickPickItem[] = kernelModels.map((kernelModel) => { - return { - label: '', - selection: { kernelModel, kernelSpec: undefined, interpreter: undefined } - }; - }); - - when( - kernelSelectionProvider.getKernelSelectionsForLocalSession( - anything(), - 'jupyter', - instance(sessionManager), - anything() - ) - ).thenResolve(quickPickItems); - when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve(undefined); - - // tslint:disable-next-line: no-any - kernelSelector.addKernelToIgnoreList({ id: 'id2' } as any); - // tslint:disable-next-line: no-any - kernelSelector.addKernelToIgnoreList({ clientId: 'id4' } as any); - const kernel = await kernelSelector.selectLocalKernel( - undefined, - 'jupyter', - new StopWatch(), - instance(sessionManager) - ); - - assert.isEmpty(kernel); - verify( - kernelSelectionProvider.getKernelSelectionsForLocalSession( - anything(), - 'jupyter', - 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 } - // tslint:disable-next-line: no-any - } as any); - - const kernel = await kernelSelector.selectLocalKernel( - undefined, - 'jupyter', - new StopWatch(), - instance(sessionManager) - ); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isOk(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 seleted 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.fallbackToUseActiveInterpeterAsKernel()) - ).thenResolve(); - when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ - selection: { interpreter, kernelSpec } - // tslint:disable-next-line: no-any - } as any); - - const kernel = await kernelSelector.selectLocalKernel( - undefined, - 'jupyter', - new StopWatch(), - instance(sessionManager) - ); - - assert.isOk(kernel.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.fallbackToUseActiveInterpeterAsKernel()) - ).never(); - verify( - appShell.showInformationMessage(localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel()) - ).never(); - }); - test('If seleted 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 } - // tslint:disable-next-line: no-any - } as any); - - const kernel = await kernelSelector.selectLocalKernel( - undefined, - 'jupyter', - new StopWatch(), - instance(sessionManager) - ); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isOk(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.fallbackToUseActiveInterpeterAsKernel()) - ).never(); - verify( - appShell.showInformationMessage(localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel()) - ).never(); - }); - test('If seleted 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 } - // tslint:disable-next-line: no-any - } as any); - - const kernel = await kernelSelector.selectLocalKernel( - undefined, - 'jupyter', - new StopWatch(), - instance(sessionManager) - ); - - assert.isOk(kernel.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.fallbackToUseActiveInterpeterAsKernel()) - ).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 } - // tslint:disable-next-line: no-any - } as any); - - const kernel = await kernelSelector.selectLocalKernel(undefined, 'raw', new StopWatch()); - - assert.isOk(kernel.interpreter === interpreter); - expect(kernel.kernelSpec, 'Should have kernelspec').to.not.be.undefined; - expect(kernel.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 } - // tslint:disable-next-line: no-any - } as any); - const kernel = await kernelSelector.selectLocalKernel(undefined, 'raw', new StopWatch()); - expect(kernel.kernelSpec).to.equal(kernelSpec); - expect(kernel.interpreter).to.equal(interpreter); - }); - }); - // tslint:disable-next-line: max-func-body-length - suite('Get a kernel for local sessions', () => { - // tslint:disable-next-line: no-any - let nbMetadataKernelSpec: nbformat.IKernelspecMetadata = {} as any; - // tslint:disable-next-line: no-any - let nbMetadata: nbformat.INotebookMetadata = {} as any; - let selectLocalKernelStub: sinon.SinonStub< - [ - Resource, - 'raw' | 'jupyter' | 'noConnection', - StopWatch, - (IJupyterSessionManager | undefined)?, - (CancellationToken | undefined)?, - string? - ], - // tslint:disable-next-line: no-any - Promise<any> - >; - setup(() => { - nbMetadataKernelSpec = { - display_name: interpreter.displayName!, - name: kernelSpec.name - }; - nbMetadata = { - // tslint:disable-next-line: no-any - 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())).thenResolve(kernelSpec); - when(kernelService.findMatchingInterpreter(kernelSpec, anything())).thenResolve(interpreter); - when( - kernelSelectionProvider.getKernelSelectionsForLocalSession( - anything(), - anything(), - anything(), - anything() - ) - ).thenResolve(); - - const kernel = await kernelSelector.getKernelForLocalConnection(anything(), 'raw', undefined, nbMetadata); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isOk(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.getKernelForLocalConnection( - anything(), - 'jupyter', - instance(sessionManager), - nbMetadata - ); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isOk(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( - kernelSelectionProvider.getKernelSelectionsForLocalSession( - anything(), - anything(), - anything(), - anything() - ) - ).thenResolve(); - - const kernel = await kernelSelector.getKernelForLocalConnection( - undefined, - 'jupyter', - instance(sessionManager), - nbMetadata - ); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isUndefined(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.fallbackToUseActiveInterpeterAsKernel()) - ).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.getKernelForLocalConnection( - undefined, - 'jupyter', - instance(sessionManager), - nbMetadata - ); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isOk(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 interperter 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.getKernelForLocalConnection( - undefined, - 'jupyter', - instance(sessionManager), - undefined - ); - - assert.isOk(kernel.kernelSpec === kernelSpec); - assert.isOk(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.getKernelForRemoteConnection( - undefined, - instance(sessionManager), - undefined - ); - - assert.ok(kernel.kernelSpec, 'No kernel spec found for remote'); - assert.equal(kernel.kernelSpec?.display_name, 'foo', 'Did not find the python kernel spec'); - assert.isOk(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.getKernelForRemoteConnection(undefined, instance(sessionManager), { - orig_nbformat: 4, - kernelspec: { display_name: 'foo', name: 'foo' } - }); - - assert.ok(kernel.kernelSpec, 'No kernel spec found for remote'); - assert.equal(kernel.kernelSpec?.display_name, 'foo', 'Did not find the preferred python kernel spec'); - assert.isOk(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.getKernelForRemoteConnection(undefined, instance(sessionManager), { - orig_nbformat: 4, - kernelspec: { display_name: 'foo', name: 'foo' } - }); - - assert.ok(kernel.kernelSpec, 'No kernel spec found for remote'); - assert.equal(kernel.kernelSpec?.display_name, 'foo', 'Did not find the preferred python kernel spec'); - assert.isOk(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 deleted file mode 100644 index 8b47c8886906..000000000000 --- a/src/test/datascience/jupyter/kernels/kernelService.unit.test.ts +++ /dev/null @@ -1,608 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - path: 'some Path', - displayName: 'Hello World', - envName: 'Hello', - type: InterpreterType.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: PythonInterpreter = { - path: 'some Path', - displayName: 'Hello World', - envName: 'Hello', - type: InterpreterType.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: PythonInterpreter = { - displayName: 'Disp2', - path: 'myPath2', - sysPrefix: 'xyz', - type: InterpreterType.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: PythonInterpreter = { - displayName: 'disp2', - path: 'Some Path2', - sysPrefix: 'xyz', - type: InterpreterType.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< - [PythonInterpreter, IJupyterSessionManager?, (CancellationToken | undefined)?], - Promise<IJupyterKernelSpec | undefined> - >; - let fakeTimer: FakeClock; - const interpreter: PythonInterpreter = { - architecture: Architecture.Unknown, - path: path.join('interpreter', 'python'), - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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.type - } - } - }; - 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: PythonInterpreter = { - architecture: Architecture.Unknown, - path: '', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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 deleted file mode 100644 index 059a8e869a25..000000000000 --- a/src/test/datascience/jupyter/kernels/kernelSwitcher.unit.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -// 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 { 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, KernelSpecInterpreter } from '../../../../client/datascience/jupyter/kernels/kernelSelector'; -import { KernelSwitcher } from '../../../../client/datascience/jupyter/kernels/kernelSwitcher'; -import { LiveKernelModel } from '../../../../client/datascience/jupyter/kernels/types'; -import { IJupyterConnection, IJupyterKernelSpec, INotebook } from '../../../../client/datascience/types'; -import { InterpreterType, PythonInterpreter } 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: PythonInterpreter; - let settings: IPythonSettings; - let newKernelSpec: KernelSpecInterpreter; - 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: '', - type: InterpreterType.Conda, - architecture: Architecture.Unknown, - sysPrefix: '', - sysVersion: '' - }; - newKernelSpec = { - kernelModel: currentKernel, - interpreter: selectedInterpreter - }; - 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 - }; - 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.getKernelSpec()).thenReturn(currentKernelInfo.currentKernel); - }); - - test('Switch to new kernel', async () => { - await kernelSwitcher.switchKernelWithRetry(instance(notebook), newKernelSpec); - verify(notebook.setKernelSpec(anything(), anything(), anything())).once(); - }); - test('Switch to new kernel with error', async () => { - const ex = new JupyterSessionStartError(new Error('Kaboom')); - when(notebook.setKernelSpec(anything(), 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), newKernelSpec); - 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 deleted file mode 100644 index 1d37c555c67b..000000000000 --- a/src/test/datascience/jupyter/serverCache.unit.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -// 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 deleted file mode 100644 index 44a17ad33dbe..000000000000 --- a/src/test/datascience/jupyter/serverSelector.unit.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -// 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); - - 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); - 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); - 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); - } - - 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 deleted file mode 100644 index fd60a39737ce..000000000000 --- a/src/test/datascience/jupyterHelpers.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index 1c90275cc011..000000000000 --- a/src/test/datascience/jupyterPasswordConnect.unit.test.ts +++ /dev/null @@ -1,298 +0,0 @@ -// 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 deleted file mode 100644 index 9511a4664414..000000000000 --- a/src/test/datascience/jupyterUriProviderRegistration.functional.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -// 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(); - ioc.registerDataScienceTypes(false); - ioc.serviceManager.rebindInstance<IExtensions>(IExtensions, new UriMockExtensions(ioc)); - return ioc.activate(); - }); - - teardown(async () => { - await ioc.dispose(); - }); - - test('Expiration', async () => { - // 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.deepEqual(authHeader, { Bearer: '2' }, 'Bearer token did not update'); - }); -}); diff --git a/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts b/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts deleted file mode 100644 index 0eda6343606d..000000000000 --- a/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -// 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 deleted file mode 100644 index 487c0205a729..000000000000 --- a/src/test/datascience/jupyterUtils.unit.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -// 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 deleted file mode 100644 index 85c5bedff3b3..000000000000 --- a/src/test/datascience/kernel-launcher/kernelDaemonPool.unit.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -// 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 { PythonInterpreter } 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, PythonInterpreter>; - - 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, PythonInterpreter>(); - 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(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 deleted file mode 100644 index ec9600b11015..000000000000 --- a/src/test/datascience/kernel-launcher/kernelDaemonPoolPreWarmer.unit.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -// 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 deleted file mode 100644 index cf1533c66507..000000000000 --- a/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -// 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 { PythonInterpreter } 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: PythonInterpreter; - 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']), deepEqual({ env: kernelSpec.env })) - ).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']), - deepEqual({ env: kernelSpec.env }) - ) - ).thenReturn(instance(observableOutputForDaemon)); - // Make sure that it doesn't have a start function. Normally the proxy will pretend to have a start function, which we are checking for non-existance - when((executionService as any).start).thenReturn(false); - // 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 deleted file mode 100644 index 1990f3cd7740..000000000000 --- a/src/test/datascience/kernelFinder.unit.test.ts +++ /dev/null @@ -1,643 +0,0 @@ -// 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 { 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter; - let interpreters: PythonInterpreter[] = []; - 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, - type: InterpreterType.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, - type: InterpreterType.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, - type: InterpreterType.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, - type: InterpreterType.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. Default spec returned should match the interpreter selected.', 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, - type: InterpreterType.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)); - }); - let spec = await kernelFinder.findKernelSpec(resource); - expect(spec.display_name).to.equal(activeInterpreter.displayName); - - spec = await kernelFinder.findKernelSpec(resource); - expect(spec.display_name).to.equal(activeInterpreter2.displayName); - 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 default', 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.equal(spec.name.includes('python_defaultSpec'), true); - fileSystem.reset(); - }); - - test('Kernel metadata already has a default spec, return the same default spec', 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.equal(spec.name.includes(defaultKernelSpecName), true); - expect(spec.display_name).to.equals('TargetDisplayName'); - fileSystem.reset(); - }); - - test('KernelSpec not found, returning default, then search for it again and find it in the cache', 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.equal(spec.name.includes('python_defaultSpec'), true); - fileSystem.reset(); - - setupFileSystem(); - fileSystem - .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) - .returns((pathParam: string) => { - if (pathParam.includes(cacheFile)) { - return Promise.resolve(`["${spec.path}"]`); - } - return Promise.resolve(JSON.stringify(spec)); - }) - .verifiable(typemoq.Times.once()); - - // get the same kernel, but from cache - const spec2 = await kernelFinder.findKernelSpec(resource, { name: spec.name, display_name: '' }); - assert.notStrictEqual(spec, spec2); - - fileSystem.verifyAll(); - 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 deleted file mode 100644 index 2492dac7014d..000000000000 --- a/src/test/datascience/kernelLauncher.functional.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { assert } 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 { PythonInterpreter } 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'; - -suite('DataScience - Kernel Launcher', () => { - let ioc: DataScienceIocContainer; - let kernelLauncher: KernelLauncher; - let pythonInterpreter: PythonInterpreter | 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.serviceContainer.get<IKernelFinder>(IKernelFinder)); - const processServiceFactory = ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); - const daemonPool = ioc.serviceContainer.get<KernelDaemonPool>(KernelDaemonPool); - const fileSystem = ioc.serviceContainer.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, undefined); - 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(spec, undefined); - 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, undefined); - - 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 deleted file mode 100644 index a8187e717761..000000000000 --- a/src/test/datascience/liveloss.py +++ /dev/null @@ -1,18 +0,0 @@ -#%% -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 deleted file mode 100644 index 0a8b6a0fa3dc..000000000000 --- a/src/test/datascience/liveshare.functional.test.tsx +++ /dev/null @@ -1,442 +0,0 @@ -// 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 deleted file mode 100644 index 9587eb752dc9..000000000000 --- a/src/test/datascience/mainState.unit.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 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 - }; - 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 deleted file mode 100644 index b6009d6f04d0..000000000000 --- a/src/test/datascience/manualTestFiles/manualTestFile.py +++ /dev/null @@ -1,75 +0,0 @@ -# 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 deleted file mode 100644 index 87de1fd22327..000000000000 --- a/src/test/datascience/manualTestFiles/manualTestFileNoCells.py +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 385165d55610..000000000000 --- a/src/test/datascience/markdownManipulation.unit.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -// 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 deleted file mode 100644 index 9b106e836b8e..000000000000 --- a/src/test/datascience/matplotlib.txt +++ /dev/null @@ -1 +0,0 @@ -<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 deleted file mode 100644 index 578e1b4c8069..000000000000 --- a/src/test/datascience/mockCode2ProtocolConverter.ts +++ /dev/null @@ -1,216 +0,0 @@ -// 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.'); - } - 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 deleted file mode 100644 index 91cd18311c8e..000000000000 --- a/src/test/datascience/mockCommandManager.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index 03ceee2b8c25..000000000000 --- a/src/test/datascience/mockCustomEditorService.ts +++ /dev/null @@ -1,180 +0,0 @@ -// 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 deleted file mode 100644 index 30f993b744fb..000000000000 --- a/src/test/datascience/mockDebugService.ts +++ /dev/null @@ -1,103 +0,0 @@ -// 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/mockExtensions.ts b/src/test/datascience/mockExtensions.ts deleted file mode 100644 index 9b25c811fd7e..000000000000 --- a/src/test/datascience/mockExtensions.ts +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index 983a8b3c834c..000000000000 --- a/src/test/datascience/mockFileSystem.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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> { - return this.readLocalFile(filePath.fsPath); - } - 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 deleted file mode 100644 index 3523093ef62c..000000000000 --- a/src/test/datascience/mockInputBox.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index 55bd905afa71..000000000000 --- a/src/test/datascience/mockJupyterManager.ts +++ /dev/null @@ -1,893 +0,0 @@ -// 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 { CodeSnippits, Identifiers } from '../../client/datascience/constants'; -import { - ICell, - IJupyterConnection, - IJupyterKernel, - IJupyterKernelSpec, - IJupyterSession, - IJupyterSessionManager -} from '../../client/datascience/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { IServiceManager } from '../../client/ioc/types'; -import { PythonInterpreter } from '../../client/pythonEnvironments/info'; -import { concatMultilineStringInput } 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: PythonInterpreter[] = []; - private pythonServices: MockPythonService[] = []; - private activeInterpreter: PythonInterpreter | 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(CodeSnippits.MatplotLibInitSvg); - this.addCell(CodeSnippits.MatplotLibInitPng); - this.addCell(CodeSnippits.ConfigSvg); - this.addCell(CodeSnippits.ConfigPng); - this.addCell(CodeSnippits.UpdateCWDAndPath.format(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'))); - this.addCell( - CodeSnippits.UpdateCWDAndPath.format( - Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')).fsPath - ) - ); - tmp.file((_e, p, _fd, cleanup) => { - this.addCell(CodeSnippits.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: PythonInterpreter) { - 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: PythonInterpreter, - 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 = concatMultilineStringInput(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 = concatMultilineStringInput(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(concatMultilineStringInput(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(_kernelSpec: IJupyterKernelSpec, 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(CodeSnippits.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: PythonInterpreter, - 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: PythonInterpreter, - 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 deleted file mode 100644 index fec36809d20f..000000000000 --- a/src/test/datascience/mockJupyterManagerFactory.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index 4be1a4a9a9cd..000000000000 --- a/src/test/datascience/mockJupyterNotebook.ts +++ /dev/null @@ -1,219 +0,0 @@ -// 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 { LiveKernelModel } from '../../client/datascience/jupyter/kernels/types'; -import { - ICell, - ICellHashProvider, - IJupyterKernelSpec, - INotebook, - INotebookCompletion, - INotebookExecutionLogger, - INotebookProviderConnection, - InterruptResult, - KernelSocketInformation -} from '../../client/datascience/types'; -import { PythonInterpreter } 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 kernelSocket = new Observable<KernelSocketInformation | undefined>(); - 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 resource(): Resource { - return Uri.file('foo.py'); - } - public onKernelChanged: Event<IJupyterKernelSpec | LiveKernelModel> = new EventEmitter< - IJupyterKernelSpec | LiveKernelModel - >().event; - public onDisposed = new EventEmitter<void>().event; - public onKernelRestarted = new EventEmitter<void>().event; - public get onKernelInterrupted(): Event<void> { - return this.kernelInterrupted.event; - } - 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(): PythonInterpreter | undefined { - return; - } - - public setInterpreter(_inter: PythonInterpreter) { - noop(); - } - - public getKernelSpec(): IJupyterKernelSpec | undefined { - return; - } - - public setKernelSpec(_spec: IJupyterKernelSpec | LiveKernelModel, _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 deleted file mode 100644 index c2bdc8011a36..000000000000 --- a/src/test/datascience/mockJupyterRequest.ts +++ /dev/null @@ -1,351 +0,0 @@ -// 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 { concatMultilineStringInput } 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: concatMultilineStringInput(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 deleted file mode 100644 index f0fd195408a5..000000000000 --- a/src/test/datascience/mockJupyterServer.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 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 { - 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.kernelSpec) { - this.launchInfo = launchInfo; - - // Validate connection info and kernel spec - if ( - launchInfo.connectionInfo.baseUrl && - launchInfo.kernelSpec.name && - /[a-z,A-Z,0-9,-,.,_]+/.test(launchInfo.kernelSpec.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 deleted file mode 100644 index 43df42753eb7..000000000000 --- a/src/test/datascience/mockJupyterSession.ts +++ /dev/null @@ -1,294 +0,0 @@ -// 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 { LiveKernelModel } from '../../client/datascience/jupyter/kernels/types'; -import { ICell, IJupyterKernelSpec, 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 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 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 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(kernel: IJupyterKernelSpec | LiveKernelModel, _timeoutMS: number): Promise<void> { - if (this.pendingKernelChangeFailure) { - this.pendingKernelChangeFailure = false; - return Promise.reject(new JupyterInvalidKernelError(kernel)); - } - 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 deleted file mode 100644 index 47036e255a0a..000000000000 --- a/src/test/datascience/mockKernelFinder.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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> { - 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 deleted file mode 100644 index 4b0df44df306..000000000000 --- a/src/test/datascience/mockLanguageClient.ts +++ /dev/null @@ -1,295 +0,0 @@ -// 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 deleted file mode 100644 index 9f7f29086519..000000000000 --- a/src/test/datascience/mockLanguageServer.ts +++ /dev/null @@ -1,148 +0,0 @@ -// 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 deleted file mode 100644 index f030728d8ab8..000000000000 --- a/src/test/datascience/mockLanguageServerAnalysisOptions.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index a730c038e400..000000000000 --- a/src/test/datascience/mockLanguageServerCache.ts +++ /dev/null @@ -1,23 +0,0 @@ -// 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 { PythonInterpreter } 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?: PythonInterpreter | 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 deleted file mode 100644 index 2e96bf58f5aa..000000000000 --- a/src/test/datascience/mockLanguageServerProxy.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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 { PythonInterpreter } 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: PythonInterpreter | 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 deleted file mode 100644 index aacf6b4546e7..000000000000 --- a/src/test/datascience/mockLiveShare.ts +++ /dev/null @@ -1,446 +0,0 @@ -// 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 deleted file mode 100644 index 7b795902a141..000000000000 --- a/src/test/datascience/mockProcessService.ts +++ /dev/null @@ -1,108 +0,0 @@ -// 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 deleted file mode 100644 index 6eea73f41eac..000000000000 --- a/src/test/datascience/mockProtocol2CodeConverter.ts +++ /dev/null @@ -1,379 +0,0 @@ -// 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), this.asPosition(value.end)); - } - 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.'); - } - 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 deleted file mode 100644 index a47f8efe4040..000000000000 --- a/src/test/datascience/mockPythonService.ts +++ /dev/null @@ -1,83 +0,0 @@ -// 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, PythonInterpreter } from '../../client/pythonEnvironments/info'; -import { MockProcessService } from './mockProcessService'; - -export class MockPythonService implements IPythonExecutionService { - private interpreter: PythonInterpreter; - private procService: MockProcessService = new MockProcessService(); - - constructor(interpreter: PythonInterpreter) { - 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 deleted file mode 100644 index 956d9ec5d65c..000000000000 --- a/src/test/datascience/mockPythonSettings.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index ad9d85059542..000000000000 --- a/src/test/datascience/mockQuickPick.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index 7f96760e1aa4..000000000000 --- a/src/test/datascience/mockStatusProvider.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 88a8019c0bc4..000000000000 --- a/src/test/datascience/mockTextEditor.ts +++ /dev/null @@ -1,110 +0,0 @@ -// 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 deleted file mode 100644 index 12d5b8d3c268..000000000000 --- a/src/test/datascience/mockWorkspaceConfig.ts +++ /dev/null @@ -1,61 +0,0 @@ -// 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 deleted file mode 100644 index 2237c9c2b02a..000000000000 --- a/src/test/datascience/mockWorkspaceConfiguration.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index 3b56a9860bf5..000000000000 --- a/src/test/datascience/mockWorkspaceFolder.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 6da22b1a0693..000000000000 --- a/src/test/datascience/mountedWebView.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { ReactWrapper } from 'enzyme'; -import { noop } from 'lodash'; -import { Event, EventEmitter, Uri } from 'vscode'; -import { - IWebPanel, - IWebPanelMessageListener, - IWebPanelOptions, - WebPanelMessage -} from '../../client/common/application/types'; -import { 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 IWebPanel, IDisposable { - readonly id: string; - readonly wrapper: ReactWrapper<any, Readonly<{}>, React.Component>; - readonly onDisposed: Event<void>; - postMessage(ev: WebPanelMessage): 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: IWebPanelOptions): 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: IWebPanelMessageListener | 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: IWebPanelOptions) { - 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: WebPanelMessage): 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.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 deleted file mode 100644 index 107a2c7d404f..000000000000 --- a/src/test/datascience/mountedWebViewFactory.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 81dec4d62b63..000000000000 --- a/src/test/datascience/nativeEditor.functional.test.tsx +++ /dev/null @@ -1,2550 +0,0 @@ -// 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 { Disposable, TextDocument, TextEditor, Uri, WindowState } from 'vscode'; -import { - IApplicationShell, - ICommandManager, - ICustomEditorService, - IDocumentManager, - IWorkspaceService -} from '../../client/common/application/types'; -import { LocalZMQKernel } from '../../client/common/experiments/groups'; -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 { - ICell, - IDataScienceErrorHandler, - IJupyterExecution, - INotebookEditorProvider, - INotebookExporter, - ITrustService -} from '../../client/datascience/types'; -import { concatMultilineStringInput } 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, - verifyHtmlOnCell -} from './testHelpers'; - -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('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(invalidKernel, undefined); - - // 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 () => { - 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.find((s) => s.selection.kernelModel?.numberOfConnections); - if (existing) { - return existing; - } - } - ); - 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: KernelSpecInterpreter = {}; - // 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(concatMultilineStringInput(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(concatMultilineStringInput(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 = concatMultilineStringInput(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('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 deleted file mode 100644 index eaee1f433ffb..000000000000 --- a/src/test/datascience/nativeEditor.toolbar.functional.test.tsx +++ /dev/null @@ -1,295 +0,0 @@ -// 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, - shouldShowTrustMessage: 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, - shouldShowTrustMessage: true - }; - }); - 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 deleted file mode 100644 index 6a7d7347f462..000000000000 --- a/src/test/datascience/nativeEditorTestHelpers.tsx +++ /dev/null @@ -1,113 +0,0 @@ -// 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 deleted file mode 100644 index b418cc3dfe98..000000000000 --- a/src/test/datascience/nativeEditorViewTracker.unit.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -// 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); - editor2 = mock(NativeEditor); - when(editor2.file).thenReturn(file2); - editor1 = mock(NativeEditor); - when(editor1.file).thenReturn(file1); - 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); - 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 deleted file mode 100644 index 7b473b737467..000000000000 --- a/src/test/datascience/notebook.functional.test.ts +++ /dev/null @@ -1,1469 +0,0 @@ -// 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 } 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 { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info'; -import { concatMultilineStringInput } 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 and add single quotes on expected value - const normalizedOutput = path.normalize(data).toUpperCase(); - const normalizedTarget = `'${path.normalize(expectedValue).toUpperCase()}'`; - 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 = concatMultilineStringInput(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) { - 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) { - 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) { - 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: PythonInterpreter) => any, - _thisArgs?: any, - _disposables?: Disposable[] - ): Disposable { - return { dispose: noop }; - } - public getInterpreters(_resource?: Uri): Promise<PythonInterpreter[]> { - return Promise.resolve([]); - } - public autoSetInterpreter(): Promise<void> { - throw new Error('Method not implemented'); - } - public getActiveInterpreter(_resource?: Uri): Promise<PythonInterpreter | undefined> { - return Promise.resolve(undefined); - } - public getInterpreterDetails(_pythonPath: string, _resoure?: Uri): Promise<PythonInterpreter> { - 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<PythonInterpreter>): 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: PythonInterpreter = { - path: '/foo/bar/baz/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.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 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(concatMultilineStringInput(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); - }); - }); - }); -}); diff --git a/src/test/datascience/notebook/cellOutput.ds.test.ts b/src/test/datascience/notebook/cellOutput.ds.test.ts deleted file mode 100644 index 40be422237bd..000000000000 --- a/src/test/datascience/notebook/cellOutput.ds.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -// 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 () => { - 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 () => { - 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 () => { - 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 deleted file mode 100644 index 2a4cc536262f..000000000000 --- a/src/test/datascience/notebook/contentProvider.ds.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -// 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 { 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 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 deleted file mode 100644 index c1bc5dcc987e..000000000000 --- a/src/test/datascience/notebook/contentProvider.unit.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -// 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 { 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 { NotebookCellData } from '../../../../typings/vscode-proposed'; -import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../client/common/constants'; -import { NotebookContentProvider } from '../../../client/datascience/notebook/contentProvider'; -import { NotebookEditorCompatibilitySupport } from '../../../client/datascience/notebook/notebookEditorCompatibilitySupport'; -import { INotebookStorageProvider } from '../../../client/datascience/notebookStorage/notebookStorageProvider'; -import { CellState, INotebookModel } from '../../../client/datascience/types'; -// 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: Partial<INotebookModel> = { - cells: [ - { - data: { - cell_type: 'code', - execution_count: 10, - hasExecutionOrder: true, - outputs: [], - source: 'print(1)', - metadata: {} - }, - file: 'a.ipynb', - id: 'MyCellId1', - line: 0, - state: CellState.init - }, - { - data: { - cell_type: 'markdown', - hasExecutionOrder: false, - source: '# HEAD', - metadata: {} - }, - file: 'a.ipynb', - id: 'MyCellId2', - line: 0, - state: CellState.init - } - ], - isTrusted: isNotebookTrusted - }; - when(storageProvider.getOrCreateModel(anything(), anything(), anything(), anything())).thenResolve( - (model as unknown) as INotebookModel - ); - - const notebook = await contentProvider.openNotebook(fileUri, {}); - - assert.isOk(notebook); - assert.deepEqual(notebook.languages, [PYTHON_LANGUAGE]); - // ignore metadata we add. - notebook.cells.forEach((cell) => delete cell.metadata.custom); - - 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(notebook.cells, [ - { - 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: Partial<INotebookModel> = { - metadata: { - language_info: { - name: 'csharp' - }, - orig_nbformat: 5 - }, - cells: [ - { - data: { - cell_type: 'code', - execution_count: 10, - hasExecutionOrder: true, - outputs: [], - source: 'Console.WriteLine("1")', - metadata: {} - }, - file: 'a.ipynb', - id: 'MyCellId1', - line: 0, - state: CellState.init - }, - { - data: { - cell_type: 'markdown', - hasExecutionOrder: false, - source: '# HEAD', - metadata: {} - }, - file: 'a.ipynb', - id: 'MyCellId2', - line: 0, - state: CellState.init - } - ], - isTrusted: isNotebookTrusted - }; - when(storageProvider.getOrCreateModel(anything(), anything(), anything(), anything())).thenResolve( - (model as unknown) as INotebookModel - ); - - const notebook = await contentProvider.openNotebook(fileUri, {}); - - assert.isOk(notebook); - assert.deepEqual(notebook.languages, ['csharp']); - - 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. - notebook.cells.forEach((cell: NotebookCellData) => delete cell.metadata.custom); - - assert.deepEqual(notebook.cells, [ - { - 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 deleted file mode 100644 index 03ea4f780c86..000000000000 --- a/src/test/datascience/notebook/edit.ds.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -// 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 deleted file mode 100644 index 6efc42a2c1ba..000000000000 --- a/src/test/datascience/notebook/empty.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"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/executionErrors.ds.test.ts b/src/test/datascience/notebook/executionErrors.ds.test.ts deleted file mode 100644 index f0c850755224..000000000000 --- a/src/test/datascience/notebook/executionErrors.ds.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert } from 'chai'; -import { Subject } from 'rxjs'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { commands } from 'vscode'; -import { - ICell, - IDataScienceErrorHandler, - INotebook, - INotebookEditorProvider, - INotebookProvider -} from '../../../client/datascience/types'; -import { IExtensionTestApi, waitForCondition } from '../../common'; -import { initialize, initializeTest } from '../../initialize'; -import { canRunTests, closeNotebooksAndCleanUpAfterTests, insertPythonCellAndWait, trustAllNotebooks } from './helper'; - -// tslint:disable: no-any no-invalid-this -suite('DataScience - VSCode Notebook - Errors in Execution', function () { - this.timeout(15_000); - - let api: IExtensionTestApi; - let editorProvider: INotebookEditorProvider; - let handleErrorStub: sinon.SinonStub<[Error], Promise<void>>; - let errorHandler: IDataScienceErrorHandler; - let notebook: INotebook; - suiteSetup(async function () { - this.timeout(15_000); - api = await initialize(); - if (!(await canRunTests())) { - return this.skip(); - } - }); - setup(async () => { - sinon.restore(); - await initializeTest(); - await trustAllNotebooks(); - const notebookProvider = api.serviceContainer.get<INotebookProvider>(INotebookProvider); - notebook = mock<INotebook>(); - (instance(notebook) as any).then = undefined; - sinon.stub(notebookProvider, 'getOrCreateNotebook').resolves(instance(notebook)); - - editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); - errorHandler = api.serviceContainer.get<IDataScienceErrorHandler>(IDataScienceErrorHandler); - handleErrorStub = sinon.stub(errorHandler, 'handleError'); - handleErrorStub.resolves(); - }); - teardown(closeNotebooksAndCleanUpAfterTests); - suiteTeardown(closeNotebooksAndCleanUpAfterTests); - - test('Errors thrown while starting a cell execution are handled by error handler', async () => { - // Open the notebook - await editorProvider.createNew(); - await insertPythonCellAndWait('#'); - - // Run a cell (with a mock notebook). - const error = new Error('MyError'); - when(notebook.executeObservable(anything(), anything(), anything(), anything(), anything())).thenThrow(error); - await commands.executeCommand('notebook.execute'); - - await waitForCondition(async () => handleErrorStub.calledOnce, 5_000, 'handleError not called'); - assert.isTrue(handleErrorStub.calledOnceWithExactly(error)); - }); - test('Errors thrown in cell execution (jupyter results) are handled by error handler', async () => { - // Open the notebook - await editorProvider.createNew(); - await insertPythonCellAndWait('#'); - - // Run a cell (with a mock notebook). - const error = new Error('MyError'); - const subject = new Subject<ICell[]>(); - subject.error(error); - when(notebook.executeObservable(anything(), anything(), anything(), anything(), anything())).thenReturn( - subject - ); - - // Execute cells (it should throw an error). - await commands.executeCommand('notebook.execute'); - - await waitForCondition(async () => handleErrorStub.calledOnce, 5_000, 'handleError not called'); - assert.isTrue(handleErrorStub.calledOnceWithExactly(error)); - }); -}); diff --git a/src/test/datascience/notebook/executionService.ds.test.ts b/src/test/datascience/notebook/executionService.ds.test.ts deleted file mode 100644 index 091b1b79bf5b..000000000000 --- a/src/test/datascience/notebook/executionService.ds.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -// 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 { 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, - 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(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 trustAllNotebooks(); - await startJupyter(); - await trustAllNotebooks(); - sinon.restore(); - vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); - editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); - - // Open a notebook and use this for all tests in this test suite. - await editorProvider.createNew(); - }); - setup(deleteAllCellsAndWait); - suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); - - test('Execute cell using VSCode Command', async () => { - await insertPythonCellAndWait('print("Hello World")', 0); - const vscCell = vscodeNotebook.activeNotebookEditor?.document.cells!; - - await commands.executeCommand('notebook.cell.execute'); - - // Wait till execution count changes and status is success. - await waitForCondition( - async () => assertHasExecutionCompletedSuccessfully(vscCell[0]), - 15_000, - 'Cell did not get executed' - ); - }); - test('Executed events are triggered', async () => { - await insertPythonCellAndWait('print("Hello World")', 0); - const vscCell = vscodeNotebook.activeNotebookEditor?.document.cells!; - - const executed = createEventHandler(editorProvider.activeEditor!, 'executed', disposables); - const codeExecuted = createEventHandler(editorProvider.activeEditor!, 'executed', disposables); - await commands.executeCommand('notebook.cell.execute'); - - // Wait till execution count changes and status is success. - await waitForCondition( - async () => assertHasExecutionCompletedSuccessfully(vscCell[0]), - 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 vscCell = vscodeNotebook.activeNotebookEditor?.document.cells![0]; - await commands.executeCommand('notebook.cell.execute'); - - // After 2s, confirm status has remained unchanged. - await sleep(2_000); - assert.isUndefined(vscCell?.metadata.runState); - }); - test('Empty cells will not get executed when running whole document', async () => { - await insertPythonCellAndWait('', 0); - await insertPythonCellAndWait('print("Hello World")', 1); - const vscCells = vscodeNotebook.activeNotebookEditor?.document.cells!; - - await commands.executeCommand('notebook.execute'); - - // Wait till execution count changes and status is success. - await waitForCondition( - async () => assertHasExecutionCompletedSuccessfully(vscCells[1]), - 15_000, - 'Cell did not get executed' - ); - assert.isUndefined(vscCells[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 vscCell = vscodeNotebook.activeNotebookEditor?.document.cells!; - const changedEvent = createEventHandler(contentProvider, 'onDidChangeNotebook', disposables); - - await commands.executeCommand('notebook.cell.execute'); - - // Wait till execution count changes and status is success. - await waitForCondition( - async () => assertHasExecutionCompletedSuccessfully(vscCell[0]), - 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!; - - editorProvider.activeEditor!.runAllCells(); - - // Wait till execution count changes and status is success. - await waitForCondition( - async () => assertHasExecutionCompletedSuccessfully(cell[0]), - 15_000, - 'Cell did not get executed' - ); - - // Verify output. - assertHasTextOutputInVSCode(cell[0], 'Hello World', 0); - - // Verify execution count. - assert.ok(cell[0].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!; - - editorProvider.activeEditor!.runAllCells(); - - // 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 commands.executeCommand('notebook.execute'); - - // 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 commands.executeCommand('notebook.execute'); - - // 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('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 commands.executeCommand('notebook.execute'); - - // 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 deleted file mode 100644 index da0172c5a0f0..000000000000 --- a/src/test/datascience/notebook/helper.ts +++ /dev/null @@ -1,437 +0,0 @@ -// 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() { - const { editorProvider } = await getServices(); - await closeActiveWindows(); - - const disposables: IDisposable[] = []; - try { - const templateIPynb = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'datascience', - 'notebook', - 'empty.ipynb' - ); - const tempIPynb = await createTemporaryNotebook(templateIPynb, disposables); - await editorProvider.open(Uri.file(tempIPynb)); - await insertPythonCell('print("Hello World")', 0); - const model = editorProvider.activeEditor?.model; - editorProvider.activeEditor?.runAllCells(); - // Wait for 15s for Jupyter to start. - await waitForCondition( - async () => (model?.cells[0].data.outputs as []).length > 0, - 15_000, - 'Cell not executed' - ); - - await closeActiveWindows(); - } 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: ICell, - executionOrder: number | undefined, - model: INotebookModel -) { - await waitForCondition( - async () => { - if (executionOrder === undefined || executionOrder === null) { - return cell.data.execution_count === null; - } - return cell.data.execution_count === executionOrder; - }, - 1_000, - `Execution count not '${executionOrder}' for ICell ${model.cells.indexOf(cell) + 1}` - ); -} -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); -} - -export function createNotebookDocument( - model: VSCodeNotebookModel, - viewType: string = JupyterNotebookView -): NotebookDocument { - const doc: NotebookDocument = { - cells: [], - fileName: model.file.fsPath, - isDirty: false, - languages: [], - uri: model.file, - 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 = { - ...vscCell, - uri: model.file.with({ fragment: `cell${index}` }), - notebook: doc, - document: instance(mock<TextDocument>()) - }; - doc.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 deleted file mode 100644 index e35938f624de..000000000000 --- a/src/test/datascience/notebook/helpers.unit.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { nbformat } from '@jupyterlab/coreutils'; -import { assert } from 'chai'; -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 - }; - - const notebook = notebookModelToVSCNotebookData((model as unknown) as INotebookModel); - - assert.isOk(notebook); - assert.deepEqual(notebook.languages, [PYTHON_LANGUAGE]); - // ignore metadata we add. - notebook.cells.forEach((cell) => delete cell.metadata.custom); - assert.deepEqual(notebook.cells, [ - { - 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 - }; - const notebook = notebookModelToVSCNotebookData((model as unknown) as INotebookModel); - - 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}`, () => { - // If `output_type` === `exeucte_result` then we must have an execution_count. - const additionalMetadata = output_type === 'execute_result' ? { execution_count: undefined } : {}; - 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: { - ...additionalMetadata, - 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: { - ...additionalMetadata, - 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: { - ...additionalMetadata, - 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: { - ...additionalMetadata, - 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: { - ...additionalMetadata, - 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: { - ...additionalMetadata, - outputType: output_type - } - } - } - } - ] - ); - }); - }); - }); - }); -}); diff --git a/src/test/datascience/notebook/interrupRestart.ds.test.ts b/src/test/datascience/notebook/interrupRestart.ds.test.ts deleted file mode 100644 index a6f46b40397b..000000000000 --- a/src/test/datascience/notebook/interrupRestart.ds.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import { CancellationTokenSource, 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 { INotebookExecutionService } from '../../../client/datascience/notebook/types'; -import { INotebookEditorProvider } from '../../../client/datascience/types'; -import { IExtensionTestApi, waitForCondition } from '../../common'; -import { initialize } from '../../initialize'; -import { - assertVSCCellHasErrors, - assertVSCCellIsNotRunning, - assertVSCCellIsRunning, - canRunTests, - closeNotebooks, - closeNotebooksAndCleanUpAfterTests, - deleteAllCellsAndWait, - 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 executionService: INotebookExecutionService; - 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(); - vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); - editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); - editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); - executionService = api.serviceContainer.get<INotebookExecutionService>(INotebookExecutionService); - }); - 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 cancellation = new CancellationTokenSource(); - 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() }); - - const promise = executionService.executeCell(vscEditor.document, cell, cancellation.token); - 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. - cancellation.cancel(); - - // 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 commands.executeCommand('notebook.execute'); - - // 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 commands.executeCommand('notebook.execute'); - - // 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 deleted file mode 100644 index 5ce23ffe65ec..000000000000 --- a/src/test/datascience/notebook/notebookEditorProvider.ds.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -// 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 deleted file mode 100644 index fc7aecc05ede..000000000000 --- a/src/test/datascience/notebook/notebookStorage.unit.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -// 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 change: NotebookModelChange = { - kind: 'version', - kernelSpec: { - name: 'foo', - // tslint:disable-next-line: no-any - session: {} as any, - lastActivityTime: new Date(), - numberOfConnections: 1, - id - }, - interpreter: undefined, - 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 deleted file mode 100644 index d0069ba83f2f..000000000000 --- a/src/test/datascience/notebook/notebookTrust.ds.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -// 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 deleted file mode 100644 index db8d38d88d0f..000000000000 --- a/src/test/datascience/notebook/rendererExension.unit.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -// 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: [], - uri: Uri.file('one.ipynb'), - fileName: '', - isDirty: false, - languages: [], - metadata: {}, - viewType: JupyterNotebookView - }; - const nonJupyterNotebook: NotebookDocument = { - cells: [], - uri: Uri.file('one.xyz'), - fileName: '', - 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 deleted file mode 100644 index 02863950c268..000000000000 --- a/src/test/datascience/notebook/rendererExtensionDownloader.unit.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index a1a36597ca7a..000000000000 --- a/src/test/datascience/notebook/saving.ds.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -// 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, - 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(); - 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 commands.executeCommand('notebook.execute'); - 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 deleted file mode 100644 index 47299ade0613..000000000000 --- a/src/test/datascience/notebook/survey.unit.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -// 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 deleted file mode 100644 index 412b9c008e74..000000000000 --- a/src/test/datascience/notebook/test.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 4045d3a437f9..000000000000 Binary files a/src/test/datascience/notebook/test.png and /dev/null differ diff --git a/src/test/datascience/notebook/with3CellsAndOutput.ipynb b/src/test/datascience/notebook/with3CellsAndOutput.ipynb deleted file mode 100644 index 630d2a58718e..000000000000 --- a/src/test/datascience/notebook/with3CellsAndOutput.ipynb +++ /dev/null @@ -1,101 +0,0 @@ -{ - "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 deleted file mode 100644 index 2c5c4b3fcb5a..000000000000 --- a/src/test/datascience/notebook/withOutput.ipynb +++ /dev/null @@ -1,1371 +0,0 @@ -{ - "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 deleted file mode 100644 index 5157197f8d19..000000000000 --- a/src/test/datascience/notebook/withOutputForTrust.ipynb +++ /dev/null @@ -1,106 +0,0 @@ -{ - "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 deleted file mode 100644 index 772d898783f0..000000000000 --- a/src/test/datascience/notebook/withOutputNew.ipynb +++ /dev/null @@ -1,123 +0,0 @@ -{ - "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 deleted file mode 100644 index 970d3b15d357..000000000000 --- a/src/test/datascience/plotViewer.functional.test.tsx +++ /dev/null @@ -1,196 +0,0 @@ -// 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 deleted file mode 100644 index 20279a3fea23..000000000000 --- a/src/test/datascience/preWarmVariables.unit.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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<PythonInterpreter>; - let interpreter: PythonInterpreter; - setup(() => { - interpreter = { - architecture: Architecture.Unknown, - path: '', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.Conda - }; - onDidChangeInterpreter = new EventEmitter<PythonInterpreter>(); - 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 deleted file mode 100644 index 099fd7fdf7c1..000000000000 --- a/src/test/datascience/progress/decorators.unit.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -// 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 deleted file mode 100644 index ece6031855ef..000000000000 --- a/src/test/datascience/progress/progressReporter.unit.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// 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 deleted file mode 100644 index 0c17e1bc9ac0..000000000000 --- a/src/test/datascience/raw-kernel/rawKernel.functional.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -// 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, - ioc.get<IDataScienceFileSystem>(IDataScienceFileSystem), - undefined, - interpreter - ); - await kernelProcess.launch(); - 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 deleted file mode 100644 index 43d806423f6f..000000000000 --- a/src/test/datascience/raw-kernel/rawKernelTestHelpers.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 5ce9a617b721..000000000000 --- a/src/test/datascience/reactHelpers.ts +++ /dev/null @@ -1,548 +0,0 @@ -// 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 deleted file mode 100644 index e847f1275ca2..000000000000 --- a/src/test/datascience/remoteTestHelpers.ts +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 01a4008fdd5b..000000000000 --- a/src/test/datascience/serverConfigFiles/jcert.pem +++ /dev/null @@ -1,21 +0,0 @@ ------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 deleted file mode 100644 index 370a3c5da279..000000000000 --- a/src/test/datascience/serverConfigFiles/jkey.key +++ /dev/null @@ -1,28 +0,0 @@ ------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 deleted file mode 100644 index e4dea4f52d0e..000000000000 --- a/src/test/datascience/serverConfigFiles/remoteNoAuth.py +++ /dev/null @@ -1,4 +0,0 @@ -# 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 deleted file mode 100644 index a79b7d192561..000000000000 --- a/src/test/datascience/serverConfigFiles/remotePassword.py +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 395e1e34daec..000000000000 --- a/src/test/datascience/serverConfigFiles/remoteToken.py +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 37ddf3799ee9..000000000000 --- a/src/test/datascience/shiftEnterBanner.unit.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -// 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 deleted file mode 100644 index ec52bd7942ee..000000000000 --- a/src/test/datascience/sub/test.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index ebc746d39bb7..000000000000 --- a/src/test/datascience/testHelpers.tsx +++ /dev/null @@ -1,715 +0,0 @@ -// 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 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 deleted file mode 100644 index 909d07832ed9..000000000000 --- a/src/test/datascience/testHelpersCore.ts +++ /dev/null @@ -1,25 +0,0 @@ -// 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 deleted file mode 100644 index eeee9169bd5d..000000000000 --- a/src/test/datascience/testInteractiveWindowProvider.ts +++ /dev/null @@ -1,121 +0,0 @@ -// 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 deleted file mode 100644 index 4a4031709e2f..000000000000 --- a/src/test/datascience/testNativeEditorProvider.ts +++ /dev/null @@ -1,174 +0,0 @@ -// 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 { 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>; -} - -// 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; - } - - 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 - ) { - super( - serviceContainer, - asyncRegistry, - disposables, - workspace, - configuration, - customEditorService, - storage, - notebookProvider - ); - } -} - -@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 deleted file mode 100644 index da364bb4a88d..000000000000 --- a/src/test/datascience/testPersistentStateFactory.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index d6dc181ac4ca..000000000000 --- a/src/test/datascience/testexecutionLogger.ts +++ /dev/null @@ -1,26 +0,0 @@ -// 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 { concatMultilineStringInput } 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${concatMultilineStringInput(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 deleted file mode 100644 index 6b9b0c02335e..000000000000 --- a/src/test/datascience/trustedNotebooks.functional.test.tsx +++ /dev/null @@ -1,451 +0,0 @@ -// 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 { EnableTrustedNotebooks } from '../../client/common/experiments/groups'; -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 }); - ioc.setExperimentState(EnableTrustedNotebooks.experiment, true); - 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 deleted file mode 100644 index b6cd14e2ceca..000000000000 --- a/src/test/datascience/uiTests/helpers.ts +++ /dev/null @@ -1,147 +0,0 @@ -// 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 deleted file mode 100644 index 7747db5b3fdf..000000000000 --- a/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts +++ /dev/null @@ -1,622 +0,0 @@ -// 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 { 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); - - 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 assert.eventually.isFalse(notebookUI.cellHasOutput(3)); - - 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); - }); - - // This last part if flakey. BeakerX can fail itself loading settings - await notebookUI.executeCell(3); - await retryIfFail(async () => { - // Confirm form with fields have been rendered. - const cellOutput = await notebookUI.getCellOutput(3); - const textAreas = await cellOutput.$$('div.widget-textarea'); - assert.isAtLeast(textAreas.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 deleted file mode 100644 index b17f71509990..000000000000 --- a/src/test/datascience/uiTests/notebookHelpers.ts +++ /dev/null @@ -1,132 +0,0 @@ -// 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(); - - const port = await getFreePort({ host: 'localhost' }); - process.env.VSC_PYTHON_DS_UI_PORT = port.toString(); - traceInfo(`Notebook UI Tests: have port ${port}`); - - // 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); - - return { notebookEditor, webViewPanel, notebookUI }; -} diff --git a/src/test/datascience/uiTests/notebookUi.ts b/src/test/datascience/uiTests/notebookUi.ts deleted file mode 100644 index e6f686ac6dbe..000000000000 --- a/src/test/datascience/uiTests/notebookUi.ts +++ /dev/null @@ -1,104 +0,0 @@ -// 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 deleted file mode 100644 index ab2ff5780af2..000000000000 --- a/src/test/datascience/uiTests/notebooks/beakerx_widgets.ipynb +++ /dev/null @@ -1,70 +0,0 @@ -{ - "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)" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "from beakerx import *\n", - "f5 = EasyForm(\"form5\")\n", - "f5.addTextArea(\"field name\", value = \"5initial value5\")\n", - "f5" - ] - } - ], - "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 deleted file mode 100644 index 14f544fef83e..000000000000 --- a/src/test/datascience/uiTests/notebooks/bqplot_widgets.ipynb +++ /dev/null @@ -1,124 +0,0 @@ -{ - "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 deleted file mode 100644 index 2cf40ef38208..000000000000 --- a/src/test/datascience/uiTests/notebooks/ipySheet_widgets.ipynb +++ /dev/null @@ -1,208 +0,0 @@ -{ - "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 deleted file mode 100644 index a42becbc0da1..000000000000 --- a/src/test/datascience/uiTests/notebooks/ipyvolume_widgets.ipynb +++ /dev/null @@ -1,107 +0,0 @@ -{ - "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 deleted file mode 100644 index 383af7e1347d..000000000000 --- a/src/test/datascience/uiTests/notebooks/k3d_widgets.ipynb +++ /dev/null @@ -1,141 +0,0 @@ -{ - "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 deleted file mode 100644 index 62970d142a3d..000000000000 --- a/src/test/datascience/uiTests/notebooks/outputinteract_widgets.ipynb +++ /dev/null @@ -1,127 +0,0 @@ -{ - "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 deleted file mode 100644 index 0d9ec45b6aae..000000000000 --- a/src/test/datascience/uiTests/notebooks/pythreejs_widgets.ipynb +++ /dev/null @@ -1,156 +0,0 @@ -{ - "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 deleted file mode 100644 index 4817fea1a6b3..000000000000 --- a/src/test/datascience/uiTests/notebooks/simple_abc.ipynb +++ /dev/null @@ -1,59 +0,0 @@ -{ - "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 deleted file mode 100644 index 2b69e47fff56..000000000000 --- a/src/test/datascience/uiTests/notebooks/standard_widgets.ipynb +++ /dev/null @@ -1,151 +0,0 @@ -{ - "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 deleted file mode 100644 index 6445ba2660a9..000000000000 --- a/src/test/datascience/uiTests/recorder.ts +++ /dev/null @@ -1,117 +0,0 @@ -// 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 deleted file mode 100644 index 055d22c32c87..000000000000 --- a/src/test/datascience/uiTests/webBrowserPanel.ts +++ /dev/null @@ -1,247 +0,0 @@ -// 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 { IWebPanel, IWebPanelOptions } 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 IWebPanel, IDisposable { - private panel?: WebviewPanel; - private server?: IWebServer; - private serverUrl: string | undefined; - private loadFailedEmitter = new EventEmitter<void>(); - constructor(private readonly disposableRegistry: IDisposableRegistry, private readonly options: IWebPanelOptions) { - 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 deleted file mode 100644 index dd3b658d95f5..000000000000 --- a/src/test/datascience/uiTests/webBrowserPanelProvider.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { IWebPanel, IWebPanelOptions, IWebPanelProvider } from '../../../client/common/application/types'; -import { IDisposableRegistry } from '../../../client/common/types'; -import { WebBrowserPanel } from './webBrowserPanel'; - -@injectable() -export class WebBrowserPanelProvider implements IWebPanelProvider { - constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} - - // tslint:disable-next-line:no-any - public async create(options: IWebPanelOptions): Promise<IWebPanel> { - return new WebBrowserPanel(this.disposableRegistry, options); - } -} diff --git a/src/test/datascience/variableTestHelpers.ts b/src/test/datascience/variableTestHelpers.ts deleted file mode 100644 index a810a5d8ed1b..000000000000 --- a/src/test/datascience/variableTestHelpers.ts +++ /dev/null @@ -1,164 +0,0 @@ -// 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 deleted file mode 100644 index 59830bcda74c..000000000000 --- a/src/test/datascience/variableexplorer.functional.test.tsx +++ /dev/null @@ -1,603 +0,0 @@ -// 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 deleted file mode 100644 index a9bcc64f1a24..000000000000 --- a/src/test/debugger/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index bb291ebd172a..000000000000 --- a/src/test/debugger/common/protocolparser.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 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 index 37eedf8d316d..8b0f55986281 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -1,8 +1,6 @@ // 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'; @@ -11,14 +9,17 @@ import { ICurrentProcess, IPathUtils } from '../../client/common/types'; import { IEnvironmentVariablesService } from '../../client/common/variables/types'; import { DebugEnvironmentVariablesHelper, - IDebugEnvironmentVariablesService + 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'; +import { normCase } from '../../client/common/platform/fs-paths'; +import { IRecommendedEnvironmentService } from '../../client/interpreter/configuration/types'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Resolving Environment Variables when Debugging', () => { let ioc: UnitTestIocContainer; @@ -28,19 +29,18 @@ suite('Resolving Environment Variables when Debugging', () => { 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 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); + debugEnvParser = new DebugEnvironmentVariablesHelper(envParser, mockProcess); pathVariableName = pathUtils.getPathVariableName(); }); suiteTeardown(closeActiveWindows); @@ -49,12 +49,16 @@ suite('Resolving Environment Variables when Debugging', () => { await closeActiveWindows(); }); - function initializeDI() { + async function initializeDI() { ioc = new UnitTestIocContainer(); ioc.registerProcessTypes(); ioc.registerFileSystemTypes(); ioc.registerVariableTypes(); ioc.registerMockProcess(); + ioc.serviceManager.addSingleton<IRecommendedEnvironmentService>( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); } async function testBasicProperties(console: ConsoleType, expectedNumberOfVariables: number) { @@ -63,8 +67,7 @@ suite('Resolving Environment Variables when Debugging', () => { pythonPath: '', args: [], envFile: '', - console - // tslint:disable-next-line:no-any + console, } as any) as LaunchRequestArguments; const envVars = await debugEnvParser.getEnvironmentVariables(args); @@ -80,6 +83,27 @@ suite('Resolving Environment Variables when Debugging', () => { test('Confirm basic environment variables exist when launched in intergrated terminal', () => testBasicProperties('integratedTerminal', 2)); + test('Confirm base environment variables are merged without overwriting when provided', async () => { + const env: Record<string, string> = { DO_NOT_OVERWRITE: '1' }; + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + env, + } as any) as LaunchRequestArguments; + + const baseEnvVars = { CONDA_PREFIX: 'path/to/conda/env', DO_NOT_OVERWRITE: '0' }; + const envVars = await debugEnvParser.getEnvironmentVariables(args, baseEnvVars); + expect(envVars).not.be.undefined; + expect(Object.keys(envVars)).lengthOf(4, '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('CONDA_PREFIX', 'path/to/conda/env', 'Property not found'); + expect(envVars).to.have.property('DO_NOT_OVERWRITE', '1', 'Property not found'); + }); + test('Confirm basic environment variables exist when launched in debug console', async () => { let expectedNumberOfVariables = Object.keys(mockProcess.env).length; if (mockProcess.env['PYTHONUNBUFFERED'] === undefined) { @@ -92,9 +116,9 @@ suite('Resolving Environment Variables when Debugging', () => { }); async function testJsonEnvVariables(console: ConsoleType, expectedNumberOfVariables: number) { - const prop1 = shortid.generate(); - const prop2 = shortid.generate(); - const prop3 = shortid.generate(); + const prop1 = normCase(shortid.generate()); + const prop2 = normCase(shortid.generate()); + const prop3 = normCase(shortid.generate()); const env: Record<string, string> = {}; env[prop1] = prop1; env[prop2] = prop2; @@ -106,13 +130,11 @@ suite('Resolving Environment Variables when Debugging', () => { args: [], envFile: '', console, - env - // tslint:disable-next-line:no-any + env, } 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'); @@ -148,7 +170,7 @@ suite('Resolving Environment Variables when Debugging', () => { async function testAppendingOfPaths( console: ConsoleType, expectedNumberOfVariables: number, - removePythonPath: boolean + removePythonPath: boolean, ) { if (removePythonPath && mockProcess.env.PYTHONPATH !== undefined) { delete mockProcess.env.PYTHONPATH; @@ -173,7 +195,7 @@ suite('Resolving Environment Variables when Debugging', () => { args: [], envFile: '', console, - env + env, } as any) as LaunchRequestArguments; const envVars = await debugEnvParser.getEnvironmentVariables(args); @@ -207,7 +229,7 @@ suite('Resolving Environment Variables when Debugging', () => { // 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' + 'Variables is not a subset', ); Object.keys(mockProcess.env).forEach((key) => { if (key === pathVariableName || key === 'PYTHONPATH') { @@ -215,7 +237,7 @@ suite('Resolving Environment Variables when Debugging', () => { } expect(mockProcess.env[key]).equal( envVars[key], - `Value for the environment variable '${key}' is incorrect.` + `Value for the environment variable '${key}' is incorrect.`, ); }); } @@ -224,7 +246,6 @@ suite('Resolving Environment Variables when Debugging', () => { 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); @@ -233,7 +254,6 @@ suite('Resolving Environment Variables when Debugging', () => { 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); @@ -242,7 +262,6 @@ suite('Resolving Environment Variables when Debugging', () => { 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(); } diff --git a/src/test/debugger/extension/adapter/activator.unit.test.ts b/src/test/debugger/extension/adapter/activator.unit.test.ts deleted file mode 100644 index e449d3d7b2cb..000000000000 --- a/src/test/debugger/extension/adapter/activator.unit.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -// 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 index 29baffc613bc..cd53b41102ab 100644 --- a/src/test/debugger/extension/adapter/adapter.test.ts +++ b/src/test/debugger/extension/adapter/adapter.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as path from 'path'; import * as vscode from 'vscode'; import { openFile } from '../../../common'; @@ -19,7 +19,7 @@ function resolveWSFile(wsRoot: string, ...filePath: string[]): string { } suite('Debugger Integration', () => { - const file = resolveWSFile(WS_ROOT, 'pythonFiles', 'debugging', 'wait_for_file.py'); + const file = resolveWSFile(WS_ROOT, 'python_files', '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); @@ -28,7 +28,6 @@ suite('Debugger Integration', () => { let fix: DebuggerFixture; suiteSetup(async function () { if (IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - // tslint:disable-next-line:no-invalid-this this.skip(); } await initialize(); @@ -58,7 +57,7 @@ suite('Debugger Integration', () => { 'launch': ['launch a file', [...defaultScriptArgs, outFile]], // prettier-ignore 'attach': ['attach to a local port', defaultScriptArgs], - 'attach to PID': ['attach to a local PID', defaultScriptArgs] + 'attach to PID': ['attach to a local PID', defaultScriptArgs], // For now we do not worry about "test" debugging. }; @@ -71,7 +70,7 @@ suite('Debugger Integration', () => { } const [configName, scriptArgs] = tests[kind]; test(kind, async () => { - const session = fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); + const session = await 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 @@ -94,7 +93,7 @@ suite('Debugger Integration', () => { } const [configName, scriptArgs] = tests[kind]; test(kind, async () => { - const session = fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); + const session = await fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); const bp = session.addBreakpoint(file, 21); // line: "time.sleep()" await session.start(); await session.waitForBreakpoint(bp); diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts index bc9365483754..50984327e40d 100644 --- a/src/test/debugger/extension/adapter/factory.unit.test.ts +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -6,44 +6,52 @@ import * as assert from 'assert'; import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from '../../../../client/common/platform/fs-paths'; import * as path from 'path'; -// tslint:disable-next-line: match-default-export-name +import * as sinon from 'sinon'; import rewiremock from 'rewiremock'; import { SemVer } from 'semver'; -import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import { 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 { IPersistentStateFactory, 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 { DebugAdapterDescriptorFactory, debugStateKeys } 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 { InterpreterType } from '../../../../client/pythonEnvironments/info'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; import { clearTelemetryReporter } from '../../../../client/telemetry'; -import { EventName } from '../../../../client/telemetry/constants'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import * as pythonDebugger from '../../../../client/debugger/pythonDebugger'; -use(chaiAsPromised); +use(chaiAsPromised.default); -// tslint:disable-next-line: max-func-body-length suite('Debugging - Adapter Factory', () => { let factory: IDebugAdapterDescriptorFactory; let interpreterService: IInterpreterService; - let appShell: IApplicationShell; + let stateFactory: IPersistentStateFactory; + let state: PersistentState<boolean | undefined>; + let showErrorMessageStub: sinon.SinonStub; + let readJSONSyncStub: sinon.SinonStub; + let commandManager: ICommandManager; + let getDebugpyPathStub: sinon.SinonStub; const nodeExecutable = undefined; - const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter'); + const debugpyPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'lib', 'python', 'debugpy'); + const debugAdapterPath = path.join(debugpyPath, 'adapter'); const pythonPath = path.join('path', 'to', 'python', 'interpreter'); const interpreter = { architecture: Architecture.Unknown, path: pythonPath, sysPrefix: '', sysVersion: '', - type: InterpreterType.Unknown, - version: new SemVer('3.7.4-test') + 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; @@ -62,22 +70,36 @@ suite('Debugging - Adapter Factory', () => { setup(() => { process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState) as PersistentState<boolean | undefined>; + commandManager = mock(CommandManager); + getDebugpyPathStub = sinon.stub(pythonDebugger, 'getDebugpyPath'); + getDebugpyPathStub.resolves(debugpyPath); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + + when( + stateFactory.createGlobalPersistentState<boolean | undefined>(debugStateKeys.doNotShowAgain, false), + ).thenReturn(instance(state)); const configurationService = mock(ConfigurationService); when(configurationService.getSettings(undefined)).thenReturn(({ - experiments: { enabled: true } - // tslint:disable-next-line: no-any + experiments: { enabled: true }, } as any) as IPythonSettings); interpreterService = mock(InterpreterService); - appShell = mock(ApplicationShell); when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter); - when(interpreterService.getInterpreters(anything())).thenResolve([interpreter]); + when(interpreterService.getInterpreters(anything())).thenReturn([interpreter]); - factory = new DebugAdapterDescriptorFactory(instance(interpreterService), instance(appShell)); + factory = new DebugAdapterDescriptorFactory( + instance(commandManager), + instance(interpreterService), + instance(stateFactory), + ); }); teardown(() => { @@ -88,6 +110,7 @@ suite('Debugging - Adapter Factory', () => { Reporter.measures = []; rewiremock.disable(); clearTelemetryReporter(); + sinon.restore(); }); function createSession(config: Partial<DebugConfiguration>, workspaceFolder?: WorkspaceFolder): DebugSession { @@ -97,7 +120,8 @@ suite('Debugging - Adapter Factory', () => { name: 'python', type: 'python', workspaceFolder, - customRequest: () => Promise.resolve() + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), }; } @@ -107,7 +131,7 @@ suite('Debugging - Adapter Factory', () => { const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepEqual(descriptor, debugExecutable); + assert.deepStrictEqual(descriptor, debugExecutable); }); test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { @@ -118,7 +142,7 @@ suite('Debugging - Adapter Factory', () => { const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepEqual(descriptor, debugExecutable); + assert.deepStrictEqual(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 () => { @@ -127,17 +151,36 @@ suite('Debugging - Adapter Factory', () => { const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepEqual(descriptor, debugExecutable); + assert.deepStrictEqual(descriptor, debugExecutable); }); test('Display a message if no python interpreter is set', async () => { - when(interpreterService.getInterpreters(anything())).thenResolve([]); + when(interpreterService.getInterpreters(anything())).thenReturn([]); 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(); + sinon.assert.calledOnce(showErrorMessageStub); + }); + + test('Display a message if python version is less than 3.7', async () => { + when(interpreterService.getInterpreters(anything())).thenReturn([]); + const session = createSession({}); + const deprecatedInterpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.6.12-test'), + }; + when(state.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(deprecatedInterpreter); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + sinon.assert.calledOnce(showErrorMessageStub); }); test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { @@ -148,21 +191,21 @@ suite('Debugging - Adapter Factory', () => { // Interpreter not needed for host/port verify(interpreterService.getInterpreters(anything())).never(); - assert.deepEqual(descriptor, debugServer); + assert.deepStrictEqual(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 + 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); + assert.deepStrictEqual(descriptor, debugServer); }); test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { @@ -172,7 +215,7 @@ suite('Debugging - Adapter Factory', () => { when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepEqual(descriptor, debugExecutable); + assert.deepStrictEqual(descriptor, debugExecutable); }); test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { @@ -181,13 +224,13 @@ suite('Debugging - Adapter Factory', () => { port: undefined, processId: undefined, listen: undefined, - connect: undefined + connect: undefined, }); const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); await expect(promise).to.eventually.be.rejectedWith( - '"request":"attach" requires either "connect", "listen", or "processId"' + '"request":"attach" requires either "connect", "listen", or "processId"', ); }); @@ -196,12 +239,12 @@ suite('Debugging - Adapter Factory', () => { const debugExecutable = new DebugAdapterExecutable(pythonPath, [ debugAdapterPath, '--log-dir', - EXTENSION_ROOT_DIR + EXTENSION_ROOT_DIR, ]); const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepEqual(descriptor, debugExecutable); + assert.deepStrictEqual(descriptor, debugExecutable); }); test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { @@ -210,7 +253,7 @@ suite('Debugging - Adapter Factory', () => { const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepEqual(descriptor, debugExecutable); + assert.deepStrictEqual(descriptor, debugExecutable); }); test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { @@ -219,31 +262,54 @@ suite('Debugging - Adapter Factory', () => { const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepEqual(descriptor, debugExecutable); + assert.deepStrictEqual(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 () => { + test('Use "debugAdapterPath" 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); + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Use "debugAdapterPython" when specified', async () => { + const session = createSession({ debugAdapterPython: '/bin/custompy' }); + const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); + const customInterpreter = { + architecture: Architecture.Unknown, + path: '/bin/custompy', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.7.4-test'), + }; + when(interpreterService.getInterpreterDetails('/bin/custompy')).thenResolve(customInterpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Do not use "python" to spawn the debug adapter', async () => { + const session = createSession({ python: '/bin/custompy' }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); }); }); diff --git a/src/test/debugger/extension/adapter/logging.unit.test.ts b/src/test/debugger/extension/adapter/logging.unit.test.ts index cae4d7b19328..18fbb2b66058 100644 --- a/src/test/debugger/extension/adapter/logging.unit.test.ts +++ b/src/test/debugger/extension/adapter/logging.unit.test.ts @@ -14,7 +14,6 @@ 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; @@ -42,13 +41,14 @@ suite('Debugging - Session Logging', () => { configuration: { name: '', request: 'launch', - type: 'python' + type: 'python', }, id: id, name: 'python', type: 'python', workspaceFolder, - customRequest: () => Promise.resolve() + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), }; } diff --git a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts index cb7643397552..9f9497317417 100644 --- a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts +++ b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -4,52 +4,52 @@ 'use strict'; import * as assert from 'assert'; -import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; +import { anyString, anything, mock, 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'; +import * as browserApis from '../../../../client/common/vscodeApis/browserApis'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { IPythonSettings } from '../../../../client/common/types'; -// tslint:disable-next-line: max-func-body-length suite('Debugging - Outdated Debugger Prompt tests.', () => { let promptFactory: OutdatedDebuggerPromptFactory; - let appShell: IApplicationShell; - let browserService: IBrowserService; + let showInformationMessageStub: sinon.SinonStub; + let browserLaunchStub: sinon.SinonStub; const ptvsdOutputEvent: DebugProtocol.OutputEvent = { seq: 1, type: 'event', event: 'output', - body: { category: 'telemetry', output: 'ptvsd', data: { packageVersion: '4.3.2' } } + 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' } } + 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 + experiments: { enabled: true }, } as any) as IPythonSettings); - appShell = mock(ApplicationShell); - browserService = mock(BrowserService); - promptFactory = new OutdatedDebuggerPromptFactory(instance(appShell), instance(browserService)); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + browserLaunchStub = sinon.stub(browserApis, 'launch'); + + promptFactory = new OutdatedDebuggerPromptFactory(); }); teardown(() => { + sinon.restore(); clearTelemetryReporter(); }); @@ -58,77 +58,85 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { configuration: { name: '', request: 'launch', - type: 'python' + type: 'python', }, id: 'test1', name: 'python', type: 'python', workspaceFolder, - customRequest: () => Promise.resolve() + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), }; } test('Show prompt when attaching to ptvsd, more info is NOT clicked', async () => { - when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); - + showInformationMessageStub.returns(Promise.resolve(undefined)); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); if (prompter) { prompter.onDidSendMessage!(ptvsdOutputEvent); } - verify(browserService.launch(anyString())).never(); + browserLaunchStub.neverCalledWith(anyString()); + // First call should show info once - verify(appShell.showInformationMessage(anything(), anything())).once(); - assert(prompter); + + sinon.assert.calledOnce(showInformationMessageStub); + assert.ok(prompter); prompter!.onDidSendMessage!(ptvsdOutputEvent); // Can't use deferred promise here await sleep(1); - verify(browserService.launch(anyString())).never(); + browserLaunchStub.neverCalledWith(anyString()); // Second time it should not be called, so overall count is one. - verify(appShell.showInformationMessage(anything(), anything())).once(); + sinon.assert.calledOnce(showInformationMessageStub); }); test('Show prompt when attaching to ptvsd, more info is clicked', async () => { - when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(Common.moreInfo())); + showInformationMessageStub.returns(Promise.resolve(Common.moreInfo)); + const deferred = createDeferred(); - when(browserService.launch(anything())).thenCall(() => deferred.resolve()); + browserLaunchStub.callsFake(() => deferred.resolve()); + browserLaunchStub.onCall(1).callsFake(() => { + return new Promise(() => deferred.resolve()); + }); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); + assert.ok(prompter); prompter!.onDidSendMessage!(ptvsdOutputEvent); await deferred.promise; - verify(browserService.launch(anything())).once(); + sinon.assert.calledOnce(browserLaunchStub); + // First call should show info once - verify(appShell.showInformationMessage(anything(), anything())).once(); + sinon.assert.calledOnce(showInformationMessageStub); 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(); + sinon.assert.calledOnce(browserLaunchStub); + // Second time it should not be called, so overall count is one. - verify(appShell.showInformationMessage(anything(), anything())).once(); + sinon.assert.calledOnce(showInformationMessageStub); }); test("Don't show prompt attaching to debugpy", async () => { - when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); + showInformationMessageStub.returns(Promise.resolve(undefined)); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); + assert.ok(prompter); prompter!.onDidSendMessage!(debugpyOutputEvent); // Can't use deferred promise here await sleep(1); - verify(appShell.showInformationMessage(anything(), anything())).never(); + showInformationMessageStub.neverCalledWith(anything(), anything()); }); const someRequest: DebugProtocol.RunInTerminalRequest = { @@ -137,36 +145,36 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { command: 'runInTerminal', arguments: { cwd: '', - args: [''] - } + args: [''], + }, }; const someEvent: DebugProtocol.ContinuedEvent = { seq: 1, type: 'event', event: 'continued', - body: { threadId: 1, allThreadsContinued: true } + 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' } + 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)); + showInformationMessageStub.returns(Promise.resolve(undefined)); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); + assert.ok(prompter); prompter!.onDidSendMessage!(message); // Can't use deferred promise here await sleep(1); - verify(appShell.showInformationMessage(anything(), anything())).never(); + showInformationMessageStub.neverCalledWith(anything(), anything()); }); }); }); diff --git a/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts b/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts index f1782d5e0429..e8e2cbd5d15d 100644 --- a/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts +++ b/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts @@ -5,7 +5,6 @@ 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'; @@ -14,35 +13,35 @@ suite('External debugpy Debugger Launcher', () => { { testName: 'When path to debugpy does not contains spaces', path: path.join('path', 'to', 'debugpy'), - expectedPath: '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"' - } + 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( + const args = await launchers.getDebugpyLauncherArgs( { host: 'something', port: 1234, - waitUntilDebuggerAttaches: false + waitUntilDebuggerAttaches: false, }, - testParams.path + 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( + const args = await launchers.getDebugpyLauncherArgs( { host: 'something', port: 1234, - waitUntilDebuggerAttaches: true + waitUntilDebuggerAttaches: true, }, - testParams.path + testParams.path, ); const expectedArgs = [testParams.expectedPath, '--listen', 'something:1234', '--wait-for-client']; expect(args).to.be.deep.equal(expectedArgs); @@ -50,12 +49,3 @@ suite('External debugpy Debugger Launcher', () => { }); }); }); - -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 index e74145bb26b2..4c4deb3cb9ad 100644 --- a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts @@ -38,7 +38,7 @@ suite('Attach to process - attach process provider factory', () => { instance(commandManager), instance(platformService), instance(processServiceFactory), - disposableRegistry + disposableRegistry, ); }); @@ -46,6 +46,6 @@ suite('Attach to process - attach process provider factory', () => { factory.registerCommands(); verify(commandManager.registerCommand(Commands.PickLocalProcess, anything(), anything())).once(); - assert.equal((disposableRegistry as Disposable[]).length, 1); + assert.strictEqual((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 index 8ac667314944..64d9103f3c5d 100644 --- a/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts @@ -17,7 +17,6 @@ import { PsProcessParser } from '../../../../client/debugger/extension/attachQui 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; @@ -49,7 +48,7 @@ suite('Attach to process - process provider', () => { detail: 'launchd', id: '1', processName: 'launchd', - commandLine: 'launchd' + commandLine: 'launchd', }, { label: 'syslogd', @@ -57,7 +56,7 @@ suite('Attach to process - process provider', () => { detail: 'syslogd', id: '41', processName: 'syslogd', - commandLine: 'syslogd' + commandLine: 'syslogd', }, { label: 'kextd', @@ -65,17 +64,21 @@ suite('Attach to process - process provider', () => { detail: 'kextd', id: '146', processName: 'kextd', - commandLine: 'kextd' - } + commandLine: 'kextd', + }, ]; when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput + stdout: psOutput, }); const attachItems = await provider._getInternalProcessEntries(); verify( - processService.exec(PsProcessParser.psLinuxCommand.command, PsProcessParser.psLinuxCommand.args, anything()) + processService.exec( + PsProcessParser.psLinuxCommand.command, + PsProcessParser.psLinuxCommand.args, + anything(), + ), ).once(); assert.deepEqual(attachItems, expectedOutput); }); @@ -94,7 +97,7 @@ suite('Attach to process - process provider', () => { detail: 'launchd', id: '1', processName: 'launchd', - commandLine: 'launchd' + commandLine: 'launchd', }, { label: 'syslogd', @@ -102,7 +105,7 @@ suite('Attach to process - process provider', () => { detail: 'syslogd', id: '41', processName: 'syslogd', - commandLine: 'syslogd' + commandLine: 'syslogd', }, { label: 'kextd', @@ -110,11 +113,11 @@ suite('Attach to process - process provider', () => { detail: 'kextd', id: '146', processName: 'kextd', - commandLine: 'kextd' - } + commandLine: 'kextd', + }, ]; when(processService.exec(PsProcessParser.psDarwinCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput + stdout: psOutput, }); const attachItems = await provider._getInternalProcessEntries(); @@ -123,8 +126,8 @@ suite('Attach to process - process provider', () => { processService.exec( PsProcessParser.psDarwinCommand.command, PsProcessParser.psDarwinCommand.args, - anything() - ) + anything(), + ), ).once(); assert.deepEqual(attachItems, expectedOutput); }); @@ -151,7 +154,7 @@ ProcessId=5912\r detail: '', id: '4', processName: 'System', - commandLine: '' + commandLine: '', }, { label: 'sihost.exe', @@ -159,7 +162,7 @@ ProcessId=5912\r detail: 'sihost.exe', id: '5728', processName: 'sihost.exe', - commandLine: 'sihost.exe' + commandLine: 'sihost.exe', }, { label: 'svchost.exe', @@ -167,20 +170,20 @@ ProcessId=5912\r 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' - } + 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 + stdout: windowsOutput, }); const attachItems = await provider._getInternalProcessEntries(); verify( - processService.exec(WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, anything()) + processService.exec(WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, anything()), ).once(); assert.deepEqual(attachItems, expectedOutput); }); @@ -196,7 +199,6 @@ ProcessId=5912\r 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); @@ -216,7 +218,7 @@ ProcessId=5912\r detail: 'kextd', id: '146', processName: 'kextd', - commandLine: 'kextd' + commandLine: 'kextd', }, { label: 'launchd', @@ -224,7 +226,7 @@ ProcessId=5912\r detail: 'launchd', id: '1', processName: 'launchd', - commandLine: 'launchd' + commandLine: 'launchd', }, { label: 'syslogd', @@ -232,11 +234,11 @@ ProcessId=5912\r detail: 'syslogd', id: '41', processName: 'syslogd', - commandLine: 'syslogd' - } + commandLine: 'syslogd', + }, ]; when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput + stdout: psOutput, }); const output = await provider.getAttachItems(); @@ -259,7 +261,7 @@ ProcessId=5912\r detail: 'python', id: '96', processName: 'python', - commandLine: 'python' + commandLine: 'python', }, { label: 'python', @@ -267,7 +269,7 @@ ProcessId=5912\r detail: 'python script.py', id: '31896', processName: 'python', - commandLine: 'python script.py' + commandLine: 'python script.py', }, { label: 'kextd', @@ -275,7 +277,7 @@ ProcessId=5912\r detail: 'kextd', id: '146', processName: 'kextd', - commandLine: 'kextd' + commandLine: 'kextd', }, { label: 'launchd', @@ -283,7 +285,7 @@ ProcessId=5912\r detail: 'launchd', id: '1', processName: 'launchd', - commandLine: 'launchd' + commandLine: 'launchd', }, { label: 'syslogd', @@ -291,11 +293,11 @@ ProcessId=5912\r detail: 'syslogd', id: '41', processName: 'syslogd', - commandLine: 'syslogd' - } + commandLine: 'syslogd', + }, ]; when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput + stdout: psOutput, }); const output = await provider.getAttachItems(); @@ -304,7 +306,6 @@ ProcessId=5912\r }); }); - // tslint:disable-next-line: max-func-body-length suite('Windows getAttachItems', () => { setup(() => { when(platformService.isMac).thenReturn(false); @@ -334,7 +335,7 @@ ProcessId=5728\r detail: 'sihost.exe', id: '5728', processName: 'sihost.exe', - commandLine: 'sihost.exe' + commandLine: 'sihost.exe', }, { label: 'svchost.exe', @@ -342,7 +343,7 @@ ProcessId=5728\r detail: '', id: '5372', processName: 'svchost.exe', - commandLine: '' + commandLine: '', }, { label: 'System', @@ -350,11 +351,11 @@ ProcessId=5728\r detail: '', id: '4', processName: 'System', - commandLine: '' - } + commandLine: '', + }, ]; when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ - stdout: windowsOutput + stdout: windowsOutput, }); const output = await provider.getAttachItems(); @@ -401,7 +402,7 @@ ProcessId=8026\r id: '8026', processName: 'python.exe', commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py' + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', }, { label: 'python.exe', @@ -411,7 +412,7 @@ ProcessId=8026\r id: '6028', processName: 'python.exe', commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', }, { label: 'sihost.exe', @@ -419,7 +420,7 @@ ProcessId=8026\r detail: 'sihost.exe', id: '5728', processName: 'sihost.exe', - commandLine: 'sihost.exe' + commandLine: 'sihost.exe', }, { label: 'svchost.exe', @@ -427,7 +428,7 @@ ProcessId=8026\r detail: '', id: '5372', processName: 'svchost.exe', - commandLine: '' + commandLine: '', }, { label: 'svchost.exe', @@ -435,7 +436,7 @@ ProcessId=8026\r 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' + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', }, { label: 'System', @@ -443,11 +444,11 @@ ProcessId=8026\r detail: '', id: '4', processName: 'System', - commandLine: '' - } + commandLine: '', + }, ]; when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ - stdout: windowsOutput + stdout: windowsOutput, }); const output = await provider.getAttachItems(); diff --git a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts index e7c8b629f956..160c53a60c40 100644 --- a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts @@ -7,7 +7,6 @@ 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 = `\ @@ -26,7 +25,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'launchd', id: '1', processName: 'launchd', - commandLine: 'launchd' + commandLine: 'launchd', }, { label: 'syslogd', @@ -34,7 +33,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'syslogd', id: '41', processName: 'syslogd', - commandLine: 'syslogd' + commandLine: 'syslogd', }, { label: 'UserEventAgent', @@ -42,7 +41,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'UserEventAgent (System)', id: '42', processName: 'UserEventAgent', - commandLine: 'UserEventAgent (System)' + commandLine: 'UserEventAgent (System)', }, { label: 'uninstalld', @@ -50,7 +49,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'uninstalld', id: '45', processName: 'uninstalld', - commandLine: 'uninstalld' + commandLine: 'uninstalld', }, { label: 'kextd', @@ -58,7 +57,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'kextd', id: '146', processName: 'kextd', - commandLine: 'kextd' + commandLine: 'kextd', }, { label: 'python', @@ -66,8 +65,8 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'python script.py', id: '31896', processName: 'python', - commandLine: 'python script.py' - } + commandLine: 'python script.py', + }, ]; const output = PsProcessParser.parseProcesses(input); @@ -92,7 +91,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'launchd', id: '1', processName: 'launchd', - commandLine: 'launchd' + commandLine: 'launchd', }, { label: 'syslogd', @@ -100,7 +99,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'syslogd', id: '41', processName: 'syslogd', - commandLine: 'syslogd' + commandLine: 'syslogd', }, { label: 'UserEventAgent', @@ -108,7 +107,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'UserEventAgent (System)', id: '42', processName: 'UserEventAgent', - commandLine: 'UserEventAgent (System)' + commandLine: 'UserEventAgent (System)', }, { label: 'kextd', @@ -116,7 +115,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'kextd', id: '146', processName: 'kextd', - commandLine: 'kextd' + commandLine: 'kextd', }, { label: 'python', @@ -124,8 +123,8 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'python script.py', id: '31896', processName: 'python', - commandLine: 'python script.py' - } + commandLine: 'python script.py', + }, ]; const output = PsProcessParser.parseProcesses(input); @@ -150,7 +149,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'launchd', id: '1', processName: 'launchd', - commandLine: 'launchd' + commandLine: 'launchd', }, { label: 'syslogd', @@ -158,7 +157,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'syslogd', id: '41', processName: 'syslogd', - commandLine: 'syslogd' + commandLine: 'syslogd', }, { label: 'UserEventAgent', @@ -166,7 +165,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'UserEventAgent (System)', id: '42', processName: 'UserEventAgent', - commandLine: 'UserEventAgent (System)' + commandLine: 'UserEventAgent (System)', }, { label: 'kextd', @@ -174,7 +173,7 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'kextd', id: '146', processName: 'kextd', - commandLine: 'kextd' + commandLine: 'kextd', }, { label: 'python', @@ -182,8 +181,8 @@ suite('Attach to process - ps process parser (POSIX)', () => { detail: 'python script.py', id: '31896', processName: 'python', - commandLine: 'python script.py' - } + commandLine: 'python script.py', + }, ]; const output = PsProcessParser.parseProcesses(input); diff --git a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts index df24c9579e62..e29490c47926 100644 --- a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts @@ -7,7 +7,6 @@ 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 = ` @@ -42,7 +41,7 @@ ProcessId=6028\r\n\ detail: '', id: '4', processName: 'System', - commandLine: '' + commandLine: '', }, { label: 'svchost.exe', @@ -50,7 +49,7 @@ ProcessId=6028\r\n\ detail: '', id: '5372', processName: 'svchost.exe', - commandLine: '' + commandLine: '', }, { label: 'sihost.exe', @@ -58,7 +57,7 @@ ProcessId=6028\r\n\ detail: 'sihost.exe', id: '5728', processName: 'sihost.exe', - commandLine: 'sihost.exe' + commandLine: 'sihost.exe', }, { label: 'svchost.exe', @@ -66,7 +65,7 @@ ProcessId=6028\r\n\ 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' + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', }, { label: 'python.exe', @@ -76,8 +75,8 @@ ProcessId=6028\r\n\ id: '6028', processName: 'python.exe', commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' - } + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, ]; const output = WmicProcessParser.parseProcesses(input); @@ -120,7 +119,7 @@ ProcessId=6028\r\n\ detail: '', id: '4', processName: 'System', - commandLine: '' + commandLine: '', }, { label: 'svchost.exe', @@ -128,7 +127,7 @@ ProcessId=6028\r\n\ detail: '', id: '5372', processName: 'svchost.exe', - commandLine: '' + commandLine: '', }, { label: 'sihost.exe', @@ -136,7 +135,7 @@ ProcessId=6028\r\n\ detail: 'sihost.exe', id: '5728', processName: 'sihost.exe', - commandLine: 'sihost.exe' + commandLine: 'sihost.exe', }, { label: 'svchost.exe', @@ -144,7 +143,7 @@ ProcessId=6028\r\n\ 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' + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', }, { label: 'python.exe', @@ -154,8 +153,8 @@ ProcessId=6028\r\n\ id: '6028', processName: 'python.exe', commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' - } + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, ]; const output = WmicProcessParser.parseProcesses(input); @@ -187,7 +186,7 @@ ProcessId=6028\r\n\ detail: '', id: '4', processName: 'System', - commandLine: '' + commandLine: '', }, { label: 'conhost.exe', @@ -195,7 +194,7 @@ ProcessId=6028\r\n\ detail: 'C:\\WINDOWS\\system32\\conhost.exe', id: '5912', processName: 'conhost.exe', - commandLine: 'C:\\WINDOWS\\system32\\conhost.exe' + commandLine: 'C:\\WINDOWS\\system32\\conhost.exe', }, { label: 'python.exe', @@ -205,8 +204,8 @@ ProcessId=6028\r\n\ id: '6028', processName: 'python.exe', commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' - } + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, ]; const output = WmicProcessParser.parseProcesses(input); diff --git a/src/test/debugger/extension/banner.unit.test.ts b/src/test/debugger/extension/banner.unit.test.ts deleted file mode 100644 index e8ec8824b5b0..000000000000 --- a/src/test/debugger/extension/banner.unit.test.ts +++ /dev/null @@ -1,359 +0,0 @@ -// 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 index c945f55c4f4d..ae13ad375371 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -3,66 +3,42 @@ '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 { DebugConfiguration, Uri } from 'vscode'; 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); - } - } + class TestPythonDebugConfigurationService extends PythonDebugConfigurationService {} 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 - ); + configService = new TestPythonDebugConfigurationService(attachResolver.object, launchResolver.object); }); test('Should use attach resolver when passing attach config', async () => { const config = ({ - request: 'attach' - } as any) as AttachRequestArguments; + request: 'attach', + } as DebugConfiguration) 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()) + a.resolveDebugConfiguration(typemoq.It.isValue(folder), typemoq.It.isValue(config), typemoq.It.isAny()), ) - .returns(() => Promise.resolve(expectedConfig as any)) + .returns(() => Promise.resolve((expectedConfig as unknown) as AttachRequestArguments)) .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); + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as DebugConfiguration); expect(resolvedConfig).to.deep.equal(expectedConfig); attachResolver.verifyAll(); @@ -77,84 +53,21 @@ suite('Debugging - Configuration Service', () => { .setup((a) => a.resolveDebugConfiguration( typemoq.It.isValue(folder), - typemoq.It.isValue((config as any) as LaunchRequestArguments), - typemoq.It.isAny() - ) + typemoq.It.isValue((config as DebugConfiguration) as LaunchRequestArguments), + typemoq.It.isAny(), + ), ) - .returns(() => Promise.resolve(expectedConfig as any)) + .returns(() => Promise.resolve((expectedConfig as unknown) as LaunchRequestArguments)) .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); + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as DebugConfiguration); 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 deleted file mode 100644 index 3108676e8c5f..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -// 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 deleted file mode 100644 index 12801cda85ba..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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/launchJsonReader.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts new file mode 100644 index 000000000000..4241f3526f1a --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { assert } from 'chai'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import { getConfigurationsForWorkspace } from '../../../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; +import * as vscodeApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Launch Json Reader', () => { + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + const workspacePath = 'path/to/workspace'; + const workspaceFolder = { + name: 'workspace', + uri: Uri.file(workspacePath), + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + getConfigurationStub = sinon.stub(vscodeApis, 'getConfiguration'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Return the config in the launch.json file', async () => { + const launchPath = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + pathExistsStub.withArgs(launchPath).resolves(true); + const launchJson = `{ + "version": "0.1.0", + "configurations": [ + { + "name": "Python: Launch.json", + "type": "python", + "request": "launch", + "purpose": ["debug-test"], + }, + ] + }`; + readFileStub.withArgs(launchPath, 'utf-8').returns(launchJson); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Launch.json', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); + + test('If there is no launch.json return the config in the workspace file', async () => { + getConfigurationStub.withArgs('launch').returns({ + configurations: [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ], + }); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); +}); 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 deleted file mode 100644 index 6b62a3962f19..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts +++ /dev/null @@ -1,489 +0,0 @@ -// 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 deleted file mode 100644 index acf868fa430e..000000000000 --- a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -// 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', '--noreload'], - 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', '--noreload'], - 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', '--noreload'], - 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 deleted file mode 100644 index cb06defdeb9c..000000000000 --- a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index cc65463424b1..000000000000 --- a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -// 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', '--no-reload'], - 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', '--no-reload'], - 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', '--no-reload'], - 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 deleted file mode 100644 index b06f26311d23..000000000000 --- a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index 0e25adab85d2..000000000000 --- a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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 deleted file mode 100644 index 668439d09940..000000000000 --- a/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -// 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 deleted file mode 100644 index e0c5d656d401..000000000000 --- a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -// 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 deleted file mode 100644 index 0f7e8aedf807..000000000000 --- a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// 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 index 474e8bf55601..d557d0e6f2f4 100644 --- a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -3,72 +3,64 @@ '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 * as sinon from 'sinon'; 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'; +import { IInterpreterService } from '../../../../../client/interpreter/contracts'; +import { getInfoPerOS } from './common'; +import * as platform from '../../../../../client/common/utils/platform'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; getInfoPerOS().forEach(([osName, osType, path]) => { - if (osType === OSType.Unknown) { + if (osType === platform.OSType.Unknown) { return; } function getAvailableOptions(): string[] { const options = [DebugOptions.RedirectOutput]; - if (osType === OSType.Windows) { + if (osType === platform.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>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let getActiveTextEditorStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; 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 - ); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + debugProvider = new AttachConfigurationResolver(configurationService.object, interpreterService.object); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + getOSTypeStub = sinon.stub(platform, 'getOSType'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getOSTypeStub.returns(osType); + }); + + teardown(() => { + sinon.restore(); }); + 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>(); @@ -76,120 +68,158 @@ getInfoPerOS().forEach(([osName, osType, path]) => { 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); + getActiveTextEditorStub.returns(textEditor.object); } else { - documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); + getActiveTextEditorStub.returns(undefined); } - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) - .returns(() => documentManager.object); } + + function getClientOS() { + return osType === platform.OSType.Windows ? 'windows' : 'unix'; + } + 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); + getWorkspaceFoldersStub.returns(workspaceFolders); } + + const attach: Partial<AttachRequestArguments> = { + name: 'Python attach', + type: 'python', + request: 'attach', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + attachConfig: Partial<AttachRequestArguments>, + ) { + let config = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + attachConfig as DebugConfiguration, + ); + if (config === undefined || config === null) { + return config; + } + + config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); + if (config === undefined || config === null) { + return config; + } + + return config as AttachRequestArguments; + } + 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); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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); + const debugConfig = await resolveDebugConfiguration(undefined, { + ...attach, + connect: { host: 'localhost', port: 5678 }, + }); 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 } + const debugConfig = await resolveDebugConfiguration(undefined, { + ...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); @@ -198,13 +228,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, 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'; @@ -214,20 +245,21 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, host, - request: 'attach' - } as any) as DebugConfiguration); + }); expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; 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) { + if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { return this.skip(); } const activeFile = 'xyz.py'; @@ -237,19 +269,22 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, host, - request: 'attach' - } as any) as DebugConfiguration); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + }); + const { pathMappings } = debugConfig as AttachRequestArguments; 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); + + return undefined; }); + 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) { + if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { return this.skip(); } const activeFile = 'xyz.py'; @@ -259,19 +294,22 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, host, - request: 'attach' - } as any) as DebugConfiguration); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + }); + const { pathMappings } = debugConfig as AttachRequestArguments; 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); + + return undefined; }); + 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) { + if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { return this.skip(); } const activeFile = 'xyz.py'; @@ -282,22 +320,25 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_${new Date().toString()}`; const debugPathMappings = [ - { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' } + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, ]; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, pathMappings: debugPathMappings, host, - request: 'attach' - } as any) as DebugConfiguration); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + }); + const { pathMappings } = debugConfig as AttachRequestArguments; 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/'); + + return undefined; }); + 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) { + if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { return this.skip(); } const activeFile = 'xyz.py'; @@ -308,20 +349,24 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_${new Date().toString()}`; const debugPathMappings = [ - { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' } + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, ]; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, pathMappings: debugPathMappings, host, - request: 'attach' - } as any) as DebugConfiguration); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + }); + const { pathMappings } = debugConfig as AttachRequestArguments; const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; - expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(Uri.file(pathMappings![0].localRoot).fsPath).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + + return undefined; }); + 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')); @@ -330,17 +375,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, host, - request: 'attach' - } as any) as DebugConfiguration); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + }); + const { pathMappings } = debugConfig as AttachRequestArguments; 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'; @@ -350,17 +396,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, host, - request: 'attach' - } as any) as DebugConfiguration); + }); expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; expect(pathMappings || []).to.be.lengthOf(0); }); }); + test("Ensure 'localRoot' and 'remoteRoot' is used", async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -370,15 +417,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, 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); @@ -388,15 +436,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, 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); @@ -405,13 +454,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, 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); @@ -420,10 +470,10 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const port = 12341234; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, port, - request: 'attach' - } as any) as DebugConfiguration); + }); expect(debugConfig).to.have.property('port', port); }); @@ -434,81 +484,17 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const debugOptions = debugOptionsAvailable + .slice() + .concat(DebugOptions.Jinja, DebugOptions.Sudo) as DebugOptions[]; const expectedDebugOptions = debugOptions.slice(); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, debugOptions, - request: 'attach' - } as any) as DebugConfiguration); + }); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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 index 1c8854e5a8bb..4da645bc34ac 100644 --- a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -1,121 +1,88 @@ +/* eslint-disable class-methods-use-this */ // 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 * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { DebugConfiguration, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, 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'; +import { IInterpreterService } from '../../../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; +import * as helper from '../../../../../client/debugger/extension/configuration/resolvers/helper'; suite('Debugging - Config Resolver', () => { class BaseResolver extends BaseConfigurationResolver<AttachRequestArguments | LaunchRequestArguments> { public resolveDebugConfiguration( _folder: WorkspaceFolder | undefined, _debugConfiguration: DebugConfiguration, - _token?: CancellationToken + _token?: CancellationToken, ): Promise<AttachRequestArguments | LaunchRequestArguments | undefined> { throw new Error('Not Implemented'); } - public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { - return super.getWorkspaceFolder(folder); + + public resolveDebugConfigurationWithSubstitutedVariables( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise<AttachRequestArguments | LaunchRequestArguments | undefined> { + throw new Error('Not Implemented'); } - public getProgram(): string | undefined { - return super.getProgram(); + + public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + return BaseConfigurationResolver.getWorkspaceFolder(folder); } + public resolveAndUpdatePythonPath( - workspaceFolder: Uri | undefined, - debugConfiguration: LaunchRequestArguments - ): void { - return super.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + workspaceFolderUri: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ) { + return super.resolveAndUpdatePythonPath(workspaceFolderUri, debugConfiguration); } + public debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { - return super.debugOption(debugOptions, debugOption); + return BaseConfigurationResolver.debugOption(debugOptions, debugOption); } + public isLocalHost(hostName?: string) { - return super.isLocalHost(hostName); + return BaseConfigurationResolver.isLocalHost(hostName); } + + public isDebuggingFastAPI(debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) { + return BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration); + } + public isDebuggingFlask(debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) { - return super.isDebuggingFlask(debugConfiguration); + return BaseConfigurationResolver.isDebuggingFlask(debugConfiguration); } } let resolver: BaseResolver; - let workspaceService: IWorkspaceService; - let platformService: IPlatformService; - let documentManager: IDocumentManager; let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let getProgramStub: sinon.SinonStub; + 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); + interpreterService = mock<IInterpreterService>(); + resolver = new BaseResolver(instance(configurationService), instance(interpreterService)); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getProgramStub = sinon.stub(helper, 'getProgram'); }); - 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'); + teardown(() => { + sinon.restore(); }); - 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' }; @@ -127,15 +94,15 @@ suite('Debugging - Config Resolver', () => { [ { title: 'Should get directory of active program when there are not workspace folders', - workspaceFolders: undefined + workspaceFolders: undefined, }, - { title: 'Should get directory of active program when there are 0 workspace folders', workspaceFolders: [] } + { 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); + getProgramStub.returns(programPath); + getWorkspaceFoldersStub.returns(item.workspaceFolders); const uri = resolver.getWorkspaceFolder(undefined); @@ -147,8 +114,11 @@ suite('Debugging - Config Resolver', () => { const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; const folders: WorkspaceFolder[] = [folder]; - resolver.getProgram = () => undefined; - when(workspaceService.workspaceFolders).thenReturn(folders); + getProgramStub.returns(undefined); + + getWorkspaceFolderStub.returns(folder); + + getWorkspaceFoldersStub.returns(folders); const uri = resolver.getWorkspaceFolder(undefined); @@ -160,9 +130,11 @@ suite('Debugging - Config Resolver', () => { 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); + getProgramStub.returns(programPath); + + getWorkspaceFoldersStub.returns(folders); + + getWorkspaceFolderStub.returns(folder2); const uri = resolver.getWorkspaceFolder(undefined); @@ -174,46 +146,156 @@ suite('Debugging - Config Resolver', () => { 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); + getProgramStub.returns(programPath); + getWorkspaceFoldersStub.returns(folders); + + getWorkspaceFolderStub.returns(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('Do nothing if debug configuration is undefined', async () => { + await resolver.resolveAndUpdatePythonPath(undefined, (undefined as unknown) as LaunchRequestArguments); }); - test('Python path in debug config must point to pythonpath in settings if pythonPath in config is not set', () => { + test('python in debug config must point to pythonPath in settings if pythonPath in config is not set', async () => { const config = {}; const pythonPath = path.join('1', '2', '3'); - when(configurationService.getSettings(anything())).thenReturn({ pythonPath } as any); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); - resolver.resolveAndUpdatePythonPath(undefined, config as any); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.have.property('pythonPath', pythonPath); + expect(config).to.have.property('python', pythonPath); }); - test('Python path in debug config must point to pythonpath in settings if pythonPath in config is ${command:python.interpreterPath}', () => { + test('python in debug config must point to pythonPath in settings if pythonPath in config is ${command:python.interpreterPath}', async () => { const config = { - pythonPath: '${command:python.interpreterPath}' + python: '${command:python.interpreterPath}', }; const pythonPath = path.join('1', '2', '3'); - when(configurationService.getSettings(anything())).thenReturn({ pythonPath } as any); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config.python).to.equal(pythonPath); + }); + + test('config should only contain python and not pythonPath after resolving', async () => { + const config = { pythonPath: '${command:python.interpreterPath}', python: '${command:python.interpreterPath}' }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + }); + + test('config should convert pythonPath to python, only if python is not set', async () => { + const config = { pythonPath: '${command:python.interpreterPath}', python: undefined }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + }); + + test('config should not change python if python is different than pythonPath', async () => { + const expected = path.join('1', '2', '4'); + const config = { pythonPath: '${command:python.interpreterPath}', python: expected }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', expected); + }); + + test('config should get python from interpreter service is nothing is set', async () => { + const config = {}; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + }); + + test('config should contain debugAdapterPython and debugLauncherPython', async () => { + const config = {}; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + expect(config).to.have.property('debugAdapterPython', pythonPath); + expect(config).to.have.property('debugLauncherPython', pythonPath); + }); + + test('config should not change debugAdapterPython and debugLauncherPython if already set', async () => { + const debugAdapterPythonPath = path.join('1', '2', '4'); + const debugLauncherPythonPath = path.join('1', '2', '5'); - resolver.resolveAndUpdatePythonPath(undefined, config as any); + const config = { debugAdapterPython: debugAdapterPythonPath, debugLauncherPython: debugLauncherPythonPath }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); - expect(config.pythonPath).to.equal(pythonPath); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + expect(config).to.have.property('debugAdapterPython', debugAdapterPythonPath); + expect(config).to.have.property('debugLauncherPython', debugLauncherPythonPath); }); + + test('config should not resolve debugAdapterPython and debugLauncherPython', async () => { + const config = { + debugAdapterPython: '${command:python.interpreterPath}', + debugLauncherPython: '${command:python.interpreterPath}', + }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + expect(config).to.have.property('debugAdapterPython', pythonPath); + expect(config).to.have.property('debugLauncherPython', 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 + '::2': false, }; Object.keys(localHostTestMatrix).forEach((key) => { test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { @@ -222,19 +304,34 @@ suite('Debugging - Config Resolver', () => { expect(isLocalHost).to.equal(localHostTestMatrix[key]); }); }); + test('Is debugging fastapi=true', () => { + const config = { module: 'fastapi' }; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(true, 'not fastapi'); + }); + test('Is debugging fastapi=false', () => { + const config = { module: 'fastapi2' }; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(false, 'fastapi'); + }); + test('Is debugging fastapi=false when not defined', () => { + const config = {}; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(false, 'fastapi'); + }); test('Is debugging flask=true', () => { const config = { module: 'flask' }; - const isFlask = resolver.isDebuggingFlask(config as any); + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); expect(isFlask).to.equal(true, 'not flask'); }); test('Is debugging flask=false', () => { const config = { module: 'flask2' }; - const isFlask = resolver.isDebuggingFlask(config as any); + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); expect(isFlask).to.equal(false, 'flask'); }); test('Is debugging flask=false when not defined', () => { const config = {}; - const isFlask = resolver.isDebuggingFlask(config as any); + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); 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 index 7c4af1600474..24c0599a04a6 100644 --- a/src/test/debugger/extension/configuration/resolvers/common.ts +++ b/src/test/debugger/extension/configuration/resolvers/common.ts @@ -4,10 +4,8 @@ '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'; +import { OSType, getOSType } from '../../../../../client/common/utils/platform'; const OS_TYPE = getOSType(); @@ -22,7 +20,7 @@ interface IPathModule { type OSTestInfo = [ string, // os name OSType, - IPathModule + IPathModule, ]; // For each supported OS, provide a set of helpers to use in tests. @@ -44,11 +42,3 @@ function getPathModuleForOS(osType: OSType): IPathModule { // 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/helper.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts new file mode 100644 index 000000000000..01205fd0c87c --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/helper.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 * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { TextDocument, TextEditor } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import { getProgram } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; + +suite('Debugging - Helpers', () => { + let getActiveTextEditorStub: sinon.SinonStub; + + setup(() => { + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + }); + teardown(() => { + sinon.restore(); + }); + + 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()); + + getActiveTextEditorStub.returns(editor.object); + + const program = 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()); + getActiveTextEditorStub.returns(editor.object); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Program should return undefined if there is no active editor', () => { + getActiveTextEditorStub.returns(undefined); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); +}); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index ce0796459b32..f312c99b1cbc 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -3,57 +3,84 @@ '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 * as sinon from 'sinon'; 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'; +import { PythonPathSource } from '../../../../../client/debugger/extension/types'; +import { ConsoleType, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; +import { IInterpreterHelper, IInterpreterService } from '../../../../../client/interpreter/contracts'; +import { getInfoPerOS } from './common'; +import * as platform from '../../../../../client/common/utils/platform'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; +import { IEnvironmentActivationService } from '../../../../../client/interpreter/activation/types'; +import * as triggerApis from '../../../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; getInfoPerOS().forEach(([osName, osType, path]) => { - if (osType === OSType.Unknown) { + if (osType === platform.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>; + const envVars = { FOO: 'BAR' }; + let diagnosticsService: TypeMoq.IMock<IInvalidPythonPathInDebuggerService>; + let configService: TypeMoq.IMock<IConfigurationService>; let debugEnvHelper: TypeMoq.IMock<IDebugEnvironmentVariablesService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let environmentActivationService: TypeMoq.IMock<IEnvironmentActivationService>; + let getActiveTextEditorStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + + setup(() => { + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + getOSTypeStub = sinon.stub(platform, 'getOSType'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getOSTypeStub.returns(osType); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); + }); + + teardown(() => { + sinon.restore(); + }); + 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>(); + function getClientOS() { + return osType === platform.OSType.Windows ? 'windows' : 'unix'; + } + + function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { + environmentActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>(); + environmentActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(envVars)); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); 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())) @@ -64,25 +91,28 @@ getInfoPerOS().forEach(([osName, osType, path]) => { .returns(() => Promise.resolve(true)); const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + // interpreterService + // .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + // .returns(() => Promise.resolve({ path: pythonPath } as any)); 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())) + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve({})); debugProvider = new LaunchConfigurationResolver( - workspaceService.object, - documentManager.object, diagnosticsService.object, - platformService.object, configService.object, - debugEnvHelper.object + debugEnvHelper.object, + interpreterService.object, + environmentActivationService.object, ); } + function setupActiveEditor(fileName: string | undefined, languageId: string) { if (fileName) { const textEditor = TypeMoq.Mock.ofType<TextEditor>(); @@ -90,41 +120,79 @@ getInfoPerOS().forEach(([osName, osType, path]) => { 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); + getActiveTextEditorStub.returns(textEditor.object); } else { - documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); + getActiveTextEditorStub.returns(undefined); } } + function setupWorkspaces(folders: string[]) { const workspaceFolders = folders.map(createMoqWorkspaceFolder); - workspaceService.setup((w) => w.workspaceFolders).returns(() => workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders); + } + + const launch: LaunchRequestArguments = { + name: 'Python launch', + type: 'python', + request: 'launch', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + launchConfig: Partial<LaunchRequestArguments>, + ) { + let config = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + launchConfig as DebugConfiguration, + ); + if (config === undefined || config === null) { + return config; + } + + const interpreterPath = configService.object.getSettings(workspaceFolder ? workspaceFolder.uri : undefined) + .pythonPath; + for (const key of Object.keys(config)) { + const value = config[key]; + if (typeof value === 'string') { + config[key] = value.replace('${command:python.interpreterPath}', interpreterPath); + } + } + + config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); + if (config === undefined || config === null) { + return config; + } + + return config as LaunchRequestArguments; } + 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 - ); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, {}); 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('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); 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); + + expect(Object.keys((debugConfig as DebugConfiguration).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); @@ -132,23 +200,28 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath, workspaceFolder); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - noDebug: true - } as any) as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + noDebug: true, + }); 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('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); 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); + + expect(Object.keys((debugConfig as DebugConfiguration).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'; @@ -156,41 +229,51 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupActiveEditor(pythonFile, PYTHON_LANGUAGE); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, {}); 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('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); 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); + + expect(Object.keys((debugConfig as DebugConfiguration).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); + const debugConfig = await resolveDebugConfiguration(undefined, {}); 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('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); 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); + + expect(Object.keys((debugConfig as DebugConfiguration).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'; @@ -198,19 +281,24 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupActiveEditor(activeFile, 'javascript'); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, {}); 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('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); 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); + + expect(Object.keys((debugConfig as DebugConfiguration).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'; @@ -219,22 +307,27 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupActiveEditor(activeFile, PYTHON_LANGUAGE); setupWorkspaces([defaultWorkspace]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, {}); 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('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); 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); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); + test("Ensure 'port' is left unaltered", async () => { const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor('spam.py', PYTHON_LANGUAGE); @@ -242,13 +335,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const port = 12341234; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await 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); @@ -256,13 +349,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, 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); @@ -270,13 +364,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, 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); @@ -285,14 +380,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, 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); @@ -301,40 +397,42 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const expected = { localRoot: `Debug_PythonPath_Local_Root_${new Date().toString()}`, - remoteRoot: `Debug_PythonPath_Remote_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 debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [expected], + }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; 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', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot: '${workspaceFolder}/spam', - remoteRoot: '${workspaceFolder}/spam' - } - ] - } as any) as DebugConfiguration); + remoteRoot: '${workspaceFolder}/spam', + }, + ], + }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.be.deep.equal([ { localRoot: `${workspaceFolder.uri.fsPath}/spam`, - remoteRoot: '${workspaceFolder}/spam' - } + remoteRoot: '${workspaceFolder}/spam', + }, ]); }); + test('Ensure path mappings are not automatically added if missing', async () => { const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor('spam.py', PYTHON_LANGUAGE); @@ -342,14 +440,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', - localRoot: localRoot - } as any) as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; 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); @@ -357,15 +456,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { 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 debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + pathMappings: [], + }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; 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); @@ -373,29 +473,29 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', - localRoot: localRoot, + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, pathMappings: [ { localRoot: '/spam', - remoteRoot: '.' - } - ] - } as any) as DebugConfiguration); + remoteRoot: '.', + }, + ], + }); expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.be.deep.equal([ { localRoot: '/spam', - remoteRoot: '.' - } + 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 + if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { return this.skip(); } const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); @@ -404,28 +504,29 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot, - remoteRoot: '/app/' - } - ] - } as any) as DebugConfiguration); + remoteRoot: '/app/', + }, + ], + }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; expect(pathMappings).to.deep.equal([ { localRoot: expected, - remoteRoot: '/app/' - } + remoteRoot: '/app/', + }, ]); + return undefined; }); + 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 + if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { return this.skip(); } const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); @@ -434,48 +535,51 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot, - remoteRoot: '/app/' - } - ] - } as any) as DebugConfiguration); + remoteRoot: '/app/', + }, + ], + }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.deep.equal([ { localRoot, - remoteRoot: '/app/' - } + remoteRoot: '/app/', + }, ]); + return undefined; }); + 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', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot: '/spam', - remoteRoot: '.' - } - ] - } as any) as DebugConfiguration); + remoteRoot: '.', + }, + ], + }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.deep.equal([ { localRoot: '/spam', - remoteRoot: '.' - } + remoteRoot: '.', + }, ]); }); + test('Ensure `${command:python.interpreterPath}` is replaced with actual pythonPath', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const activeFile = 'xyz.py'; @@ -485,12 +589,37 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - pythonPath: '${command:python.interpreterPath}' - } as any) as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pythonPath: '${command:python.interpreterPath}', + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + }); + + test('Ensure `${command:python.interpreterPath}` substitution is properly handled', 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 resolveDebugConfiguration(workspaceFolder, { + ...launch, + python: '${command:python.interpreterPath}', + }); - expect(debugConfig).to.have.property('pythonPath', pythonPath); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); }); + test('Ensure hardcoded pythonPath is left unaltered', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const activeFile = 'xyz.py'; @@ -501,12 +630,80 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - pythonPath: debugPythonPath - } as any) as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pythonPath: debugPythonPath, + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', debugPythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', debugPythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', debugPythonPath); + }); + + test('Ensure hardcoded "python" 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 resolveDebugConfiguration(workspaceFolder, { + ...launch, + python: debugPythonPath, + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', debugPythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + }); + + test('Ensure hardcoded "debugAdapterPython" 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 resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugAdapterPython: debugPythonPath, + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', debugPythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + }); + + test('Ensure hardcoded "debugLauncherPython" 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 resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugLauncherPython: debugPythonPath, + }); - expect(debugConfig).to.have.property('pythonPath', debugPythonPath); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', debugPythonPath); }); + test('Test defaults of debugger', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -514,23 +711,24 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - {} as DebugConfiguration - ); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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) { + if (osType === platform.OSType.Windows) { expectedOptions.push(DebugOptions.FixFilePathCase); } - expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); }); + test('Test defaults of python debugger', async () => { - if ('python' === DebuggerTypeName) { + if (DebuggerTypeName === 'python') { return; } const pythonPath = `PythonPath_${new Date().toString()}`; @@ -539,16 +737,17 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - {} as DebugConfiguration - ); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('showReturnValue', true); expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([]); + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal([]); }); + test('Test overriding defaults of debugger', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -556,134 +755,72 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: true, - justMyCode: false - } as LaunchRequestArguments); + justMyCode: false, + }); expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); 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) { + const expectedOptions = [DebugOptions.ShowReturnValue, DebugOptions.RedirectOutput]; + if (osType === platform.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); - }); + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); }); + const testsForRedirectOutput = [ { console: 'internalConsole', redirectOutput: undefined, - expectedRedirectOutput: true + expectedRedirectOutput: true, }, { console: 'integratedTerminal', redirectOutput: undefined, - expectedRedirectOutput: undefined + expectedRedirectOutput: undefined, }, { console: 'externalTerminal', redirectOutput: undefined, - expectedRedirectOutput: undefined + expectedRedirectOutput: undefined, }, { console: 'internalConsole', redirectOutput: false, - expectedRedirectOutput: false + expectedRedirectOutput: false, }, { console: 'integratedTerminal', redirectOutput: false, - expectedRedirectOutput: false + expectedRedirectOutput: false, }, { console: 'externalTerminal', redirectOutput: false, - expectedRedirectOutput: false + expectedRedirectOutput: false, }, { console: 'internalConsole', redirectOutput: true, - expectedRedirectOutput: true + expectedRedirectOutput: true, }, { console: 'integratedTerminal', redirectOutput: true, - expectedRedirectOutput: true + expectedRedirectOutput: true, }, { console: 'externalTerminal', redirectOutput: true, - expectedRedirectOutput: true - } + expectedRedirectOutput: true, + }, ]; test('Ensure redirectOutput property is correctly derived from console type', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; @@ -692,17 +829,19 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); testsForRedirectOutput.forEach(async (testParams) => { - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { - console: testParams.console, - redirectOutput: testParams.redirectOutput - } as LaunchRequestArguments); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + console: testParams.console as ConsoleType, + redirectOutput: testParams.redirectOutput, + }); 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); + expect((debugConfig as DebugConfiguration).debugOptions).to.contain(DebugOptions.RedirectOutput); } }); }); + test('Test fixFilePathCase', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -710,16 +849,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - {} as DebugConfiguration - ); - if (osType === OSType.Windows) { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + if (osType === platform.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'); @@ -729,15 +868,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const options = { debugOptions: [DebugOptions.Pyramid], pyramid: true }; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + 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); + expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); }); + test('Auto detect flask debugging', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -745,15 +885,70 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - module: 'flask' - } as any) as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + module: 'flask', + }); expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); + expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); + }); + + test('Test validation of Python Path when launching debugger (with invalid "python")', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; + const debugAdapterPython = `DebugAdapterPythonPath_${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), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + // Invalid + .returns(() => Promise.resolve(false)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugLauncherPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugAdapterPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + python: pythonPath, + debugLauncherPython, + debugAdapterPython, + }); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.be.equal(undefined, 'Not undefined'); }); - test('Test validation of Python Path when launching debugger (with invalid python path)', async () => { + + test('Test validation of Python Path when launching debugger (with invalid "debugLauncherPython")', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; + const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; + const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupIoc(pythonPath); @@ -762,21 +957,49 @@ getInfoPerOS().forEach(([osName, osType, path]) => { diagnosticsService.reset(); diagnosticsService .setup((h) => - h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + h.validatePythonPath( + TypeMoq.It.isValue(pythonPath), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugLauncherPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + // Invalid + .returns(() => Promise.resolve(false)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugAdapterPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), ) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); + .returns(() => Promise.resolve(true)); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: false, - pythonPath - } as LaunchRequestArguments); + python: pythonPath, + debugLauncherPython, + debugAdapterPython, + }); diagnosticsService.verifyAll(); expect(debugConfig).to.be.equal(undefined, 'Not undefined'); }); - test('Test validation of Python Path when launching debugger (with valid python path)', async () => { + + test('Test validation of Python Path when launching debugger (with invalid "debugAdapterPython")', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; + const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; + const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupIoc(pythonPath); @@ -785,76 +1008,137 @@ getInfoPerOS().forEach(([osName, osType, path]) => { diagnosticsService.reset(); diagnosticsService .setup((h) => - h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + h.validatePythonPath( + TypeMoq.It.isValue(pythonPath), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugLauncherPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugAdapterPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + // Invalid + .returns(() => Promise.resolve(false)); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + python: pythonPath, + debugLauncherPython, + debugAdapterPython, + }); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.be.equal(undefined, 'Not undefined'); + }); + + test('Test validation of Python Path when launching debugger (with valid "python/debugAdapterPython/debugLauncherPython")', 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), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), ) .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); + .verifiable(TypeMoq.Times.atLeastOnce()); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: false, - pythonPath - } as LaunchRequestArguments); + python: pythonPath, + }); 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'}`; + const sep = osType === platform.OSType.Windows ? '\\' : '/'; + const expectedEnvFilePath = `${workspaceFolder.uri.fsPath}${sep}${'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()) + h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => Promise.resolve(true)); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: false, pythonPath, - envFile: path.join('${workspaceFolder}', 'wow.envFile') - } as LaunchRequestArguments); + envFile: path.join('${workspaceFolder}', 'wow.envFile'), + }); expect(debugConfig!.envFile).to.be.equal(expectedEnvFilePath); }); + async function testSetting( requestType: 'launch' | 'attach', settings: Record<string, boolean>, debugOptionName: DebugOptions, - mustHaveDebugOption: boolean + mustHaveDebugOption: boolean, ) { setupIoc('pythonPath'); - const debugConfiguration: DebugConfiguration = { + let debugConfig: DebugConfiguration = { request: requestType, type: 'python', name: '', - ...settings + ...settings, }; const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfiguration); + debugConfig = (await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfig))!; + debugConfig = (await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!( + workspaceFolder, + debugConfig, + ))!; + if (mustHaveDebugOption) { - expect((debugConfig as any).debugOptions).contains(debugOptionName); + expect(debugConfig.debugOptions).contains(debugOptionName); } else { - expect((debugConfig as any).debugOptions).not.contains(debugOptionName); + expect(debugConfig.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 () => { + 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 () => { + 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 () => { + 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/debugCommands.unit.test.ts b/src/test/debugger/extension/debugCommands.unit.test.ts new file mode 100644 index 000000000000..7d2463072f06 --- /dev/null +++ b/src/test/debugger/extension/debugCommands.unit.test.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { ICommandManager, IDebugService } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import * as telemetry from '../../../client/telemetry'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; + +suite('Debugging - commands', () => { + let commandManager: typemoq.IMock<ICommandManager>; + let debugService: typemoq.IMock<IDebugService>; + let disposables: typemoq.IMock<IDisposableRegistry>; + let interpreterService: typemoq.IMock<IInterpreterService>; + let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + + setup(() => { + commandManager = typemoq.Mock.ofType<ICommandManager>(); + commandManager + .setup((c) => c.executeCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve()); + debugService = typemoq.Mock.ofType<IDebugService>(); + disposables = typemoq.Mock.ofType<IDisposableRegistry>(); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { + /** noop */ + }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); + }); + teardown(() => { + sinon.restore(); + }); + test('Test registering debug file command', async () => { + commandManager + .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* noop */ + }, + })) + .verifiable(typemoq.Times.once()); + + debugCommands = new DebugCommands( + commandManager.object, + debugService.object, + disposables.object, + interpreterService.object, + ); + await debugCommands.activate(); + commandManager.verifyAll(); + }); + test('Test running debug file command', async () => { + let callback: (f: Uri) => Promise<void> = (_f: Uri) => Promise.resolve(); + commandManager + .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) + .callback((_name, cb) => { + callback = cb; + }); + debugService + .setup((d) => d.startDebugging(undefined, typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + debugCommands = new DebugCommands( + commandManager.object, + debugService.object, + disposables.object, + interpreterService.object, + ); + await debugCommands.activate(); + + await callback(Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'test.py'))); + commandManager.verifyAll(); + debugService.verifyAll(); + }); +}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts index 69d353da035a..b1053def2eba 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -3,14 +3,13 @@ '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'; +import { DebuggerTypeName } from '../../../../client/debugger/constants'; suite('Debug - Child Process', () => { test('Do not attach if the event is undefined', async () => { @@ -23,7 +22,15 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if debugger type is different', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: 'other-type' } }; await handler.handleCustomEvent({ event: 'abc', body, session }); verify(attachService.attach(body, session)).never(); }); @@ -31,7 +38,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -39,7 +46,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -51,11 +58,13 @@ suite('Debug - Child Process', () => { type: 'python', request: 'attach', port: 1234, - subProcessId: 2 + subProcessId: 2, + }; + const session: any = { + configuration: { type: DebuggerTypeName }, }; - const session: any = {}; when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); - await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session: {} as any }); + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); 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 index c92147569be6..118efe416e94 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -3,33 +3,31 @@ 'use strict'; -// tslint:disable:no-any max-func-body-length - import { expect } from 'chai'; +import * as sinon from 'sinon'; 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 { IDebugService } from '../../../../client/common/application/types'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; suite('Debug - Attach to Child Process', () => { - let shell: IApplicationShell; let debugService: IDebugService; - let workspaceService: IWorkspaceService; let attachService: ChildProcessAttachService; + let getWorkspaceFoldersStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; setup(() => { - shell = mock(ApplicationShell); debugService = mock(DebugService); - workspaceService = mock(WorkspaceService); - attachService = new ChildProcessAttachService( - instance(shell), - instance(debugService), - instance(workspaceService) - ); + attachService = new ChildProcessAttachService(instance(debugService)); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + }); + teardown(() => { + sinon.restore(); }); test('Message is not displayed if debugger is launched', async () => { @@ -38,18 +36,18 @@ suite('Debug - Attach to Child Process', () => { type: 'python', request: 'attach', port: 1234, - subProcessId: 2 + subProcessId: 2, }; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(true as any); - when(shell.showErrorMessage(anything())).thenResolve(); + showErrorMessageStub.returns(undefined); await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(anything(), anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Message is displayed if debugger is not launched', async () => { const data: AttachRequestArguments = { @@ -57,19 +55,19 @@ suite('Debug - Attach to Child Process', () => { type: 'python', request: 'attach', port: 1234, - subProcessId: 2 + subProcessId: 2, }; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(false as any); - when(shell.showErrorMessage(anything())).thenResolve(); + showErrorMessageStub.resolves(() => {}); await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(anything(), anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).once(); + sinon.assert.calledOnce(showErrorMessageStub); }); test('Use correct workspace folder', async () => { const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; @@ -82,19 +80,18 @@ suite('Debug - Attach to Child Process', () => { request: 'attach', port: 1234, subProcessId: 2, - workspaceFolder: rightWorkspaceFolder.uri.fsPath + workspaceFolder: rightWorkspaceFolder.uri.fsPath, }; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([wkspace1, rightWorkspaceFolder, wkspace2]); + getWorkspaceFoldersStub.returns([wkspace1, rightWorkspaceFolder, wkspace2]); when(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.called(getWorkspaceFoldersStub); verify(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Use empty workspace folder if right one is not found', async () => { const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; @@ -107,66 +104,65 @@ suite('Debug - Attach to Child Process', () => { request: 'attach', port: 1234, subProcessId: 2, - workspaceFolder: rightWorkspaceFolder.uri.fsPath + workspaceFolder: rightWorkspaceFolder.uri.fsPath, }; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([wkspace1, wkspace2]); + getWorkspaceFoldersStub.returns([wkspace1, wkspace2]); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.called(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); - test('Validate debug config is passed as is', async () => { + test('Validate debug config is passed with the correct params', async () => { const data: LaunchRequestArguments | AttachRequestArguments = { request: 'attach', type: 'python', name: 'Attach', port: 1234, subProcessId: 2, - host: 'localhost' + host: 'localhost', }; const debugConfig = JSON.parse(JSON.stringify(data)); debugConfig.host = 'localhost'; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); 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(); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); }); test('Pass data as is if data is attach debug configuration', async () => { const data: AttachRequestArguments = { type: 'python', request: 'attach', - name: '' + name: '', }; const session: any = {}; const debugConfig = JSON.parse(JSON.stringify(data)); - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); 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(); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); }); test('Validate debug config when parent/root parent was attached', async () => { const data: AttachRequestArguments = { @@ -175,7 +171,7 @@ suite('Debug - Attach to Child Process', () => { name: 'Attach', host: '123.123.123.123', port: 1234, - subProcessId: 2 + subProcessId: 2, }; const debugConfig = JSON.parse(JSON.stringify(data)); @@ -184,16 +180,16 @@ suite('Debug - Attach to Child Process', () => { debugConfig.request = 'attach'; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); 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(); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); }); }); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 53066a3d5571..056d722c7e0e 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -13,37 +11,18 @@ import { DebugSessionLoggingFactory } from '../../../client/debugger/extension/a 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 { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; +import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; 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 + IOutdatedDebuggerPromptFactory, } from '../../../client/debugger/extension/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../client/debugger/types'; import { ServiceManager } from '../../../client/ioc/serviceManager'; @@ -51,161 +30,85 @@ 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 - ) + ChildProcessAttachService, + ), ).once(); verify( serviceManager.addSingleton<IExtensionSingleActivationService>( IExtensionSingleActivationService, - LaunchJsonCompletionProvider - ) - ).once(); - verify( - serviceManager.addSingleton<IExtensionSingleActivationService>( - IExtensionSingleActivationService, - LaunchJsonUpdaterService - ) - ).once(); - verify( - serviceManager.addSingleton<IExtensionSingleActivationService>( - IExtensionSingleActivationService, - DebugAdapterActivator - ) + DebugAdapterActivator, + ), ).once(); verify( serviceManager.addSingleton<IDebugAdapterDescriptorFactory>( IDebugAdapterDescriptorFactory, - DebugAdapterDescriptorFactory - ) + DebugAdapterDescriptorFactory, + ), ).once(); verify( serviceManager.addSingleton<IDebugSessionEventHandlers>( IDebugSessionEventHandlers, - ChildProcessAttachEventHandler - ) + ChildProcessAttachEventHandler, + ), ).once(); verify( serviceManager.addSingleton<IDebugConfigurationResolver<LaunchRequestArguments>>( IDebugConfigurationResolver, LaunchConfigurationResolver, - 'launch' - ) + 'launch', + ), ).once(); verify( serviceManager.addSingleton<IDebugConfigurationResolver<AttachRequestArguments>>( IDebugConfigurationResolver, AttachConfigurationResolver, - 'attach' - ) - ).once(); - verify( - serviceManager.addSingleton<IDebugConfigurationProviderFactory>( - IDebugConfigurationProviderFactory, - DebugConfigurationProviderFactory - ) + 'attach', + ), ).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 - ) + DebugAdapterActivator, + ), ).once(); verify( serviceManager.addSingleton<IDebugAdapterDescriptorFactory>( IDebugAdapterDescriptorFactory, - DebugAdapterDescriptorFactory - ) + DebugAdapterDescriptorFactory, + ), ).once(); verify( serviceManager.addSingleton<IDebugSessionLoggingFactory>( IDebugSessionLoggingFactory, - DebugSessionLoggingFactory - ) + DebugSessionLoggingFactory, + ), ).once(); verify( serviceManager.addSingleton<IOutdatedDebuggerPromptFactory>( IOutdatedDebuggerPromptFactory, - OutdatedDebuggerPromptFactory - ) + OutdatedDebuggerPromptFactory, + ), ).once(); verify( serviceManager.addSingleton<IAttachProcessProviderFactory>( IAttachProcessProviderFactory, - AttachProcessProviderFactory - ) + AttachProcessProviderFactory, + ), + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugCommands, + ), ).once(); }); }); diff --git a/src/test/debugger/utils.ts b/src/test/debugger/utils.ts index 61496a14671f..9ccb8958b660 100644 --- a/src/test/debugger/utils.ts +++ b/src/test/debugger/utils.ts @@ -3,10 +3,8 @@ 'use strict'; -// tslint:disable:max-classes-per-file - import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../client/common/platform/fs-paths'; import * as path from 'path'; import * as vscode from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; @@ -42,17 +40,17 @@ type TrackedDebugger = { class DebugAdapterTracker { constructor( // This contains all the state. - private readonly tracked: TrackedDebugger + 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); } @@ -153,7 +151,7 @@ class Debuggers { } const result = { exitCode: tracked.exitCode, - stdout: tracked.output.stdout + stdout: tracked.output.stdout, }; this.results[id] = result; return result; @@ -184,7 +182,7 @@ class DebuggerSession { public readonly id: number, public readonly config: vscode.DebugConfiguration, private readonly wsRoot?: vscode.WorkspaceFolder, - private readonly proc?: Proc + private readonly proc?: Proc, ) {} public async start() { @@ -215,7 +213,7 @@ class DebuggerSession { 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) + new vscode.Position(line - 1, ch ? ch - 1 : 0), ); const bp = new vscode.SourceBreakpoint(loc); vscode.debug.addBreakpoints([bp]); @@ -240,7 +238,7 @@ class DebuggerSession { const msg = (baseMsg as unknown) as DebugProtocol.StoppedEvent; this.stopped = { breakpoint: msg.body.reason === 'breakpoint', - threadId: (msg.body.threadId as unknown) as number + threadId: (msg.body.threadId as unknown) as number, }; } else { // For now there aren't any other events we care about. @@ -279,12 +277,12 @@ class DebuggerSession { } export class DebuggerFixture extends PythonFixture { - public resolveDebugger( + public async resolveDebugger( configName: string, file: string, scriptArgs: string[], - wsRoot?: vscode.WorkspaceFolder - ): DebuggerSession { + wsRoot?: vscode.WorkspaceFolder, + ): Promise<DebuggerSession> { const config = getConfig(configName); let proc: Proc | undefined; if (config.request === 'launch') { @@ -294,7 +292,7 @@ export class DebuggerFixture extends PythonFixture { // XXX set the file in the current vscode editor? } else if (config.request === 'attach') { if (config.port) { - proc = this.runDebugger(config.port, file, ...scriptArgs); + proc = await this.runDebugger(config.port, file, ...scriptArgs); if (wsRoot && config.name === 'attach to a local port') { config.pathMappings.localRoot = wsRoot.uri.fsPath; } @@ -323,7 +321,7 @@ export class DebuggerFixture extends PythonFixture { request: 'launch', program: filename, args: args, - console: 'integratedTerminal' + console: 'integratedTerminal', }; } @@ -338,11 +336,10 @@ export class DebuggerFixture extends PythonFixture { host: 'localhost', pathMappings: [ { - // tslint:disable-next-line:no-invalid-template-strings localRoot: '${workspaceFolder}', - remoteRoot: '.' - } - ] + remoteRoot: '.', + }, + ], }; } else { const proc = this.runScript(filename, ...args); @@ -350,17 +347,17 @@ export class DebuggerFixture extends PythonFixture { type: 'python', name: 'debug', request: 'attach', - processId: proc.pid + processId: proc.pid, }; } } - public runDebugger(port: number, filename: string, ...scriptArgs: string[]) { - const args = getDebugpyLauncherArgs({ + public async runDebugger(port: number, filename: string, ...scriptArgs: string[]) { + const args = await getDebugpyLauncherArgs({ host: 'localhost', port: port, // This causes problems if we set it to true. - waitUntilDebuggerAttaches: false + 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 index c7fc3e1058d1..949f14caee3d 100644 --- a/src/test/debuggerTest.ts +++ b/src/test/debuggerTest.ts @@ -1,11 +1,10 @@ // 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 { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; @@ -18,8 +17,8 @@ function start() { 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' } + version: getChannel(), + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Debugger tests (with errors)', ex); process.exit(1); diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts new file mode 100644 index 000000000000..2e5d13161f7b --- /dev/null +++ b/src/test/environmentApi.unit.test.ts @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import { assert, expect } from 'chai'; +import { Uri, EventEmitter, ConfigurationTarget, WorkspaceFolder } from 'vscode'; +import { cloneDeep } from 'lodash'; +import { + IConfigurationService, + IDisposableRegistry, + IExtensions, + IInterpreterPathService, + IPythonSettings, + Resource, +} from '../client/common/types'; +import { IServiceContainer } from '../client/ioc/types'; +import { + buildEnvironmentApi, + convertCompleteEnvInfo, + convertEnvInfo, + EnvironmentReference, + reportActiveInterpreterChanged, +} from '../client/environmentApi'; +import { IDiscoveryAPI, ProgressNotificationEvent } from '../client/pythonEnvironments/base/locator'; +import { buildEnvInfo } from '../client/pythonEnvironments/base/info/env'; +import { sleep } from './core'; +import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; +import { Architecture } from '../client/common/utils/platform'; +import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/base/watcher'; +import { normCasePath } from '../client/common/platform/fs-paths'; +import { IWorkspaceService } from '../client/common/application/types'; +import { IEnvironmentVariablesProvider } from '../client/common/variables/types'; +import * as workspaceApis from '../client/common/vscodeApis/workspaceApis'; +import { + ActiveEnvironmentPathChangeEvent, + EnvironmentVariablesChangeEvent, + EnvironmentsChangeEvent, + PythonExtension, +} from '../client/api/types'; +import { JupyterPythonEnvironmentApi } from '../client/jupyter/jupyterIntegration'; + +suite('Python Environment API', () => { + const workspacePath = 'path/to/workspace'; + const workspaceFolder = { + name: 'workspace', + uri: Uri.file(workspacePath), + index: 0, + }; + let serviceContainer: typemoq.IMock<IServiceContainer>; + let discoverAPI: typemoq.IMock<IDiscoveryAPI>; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; + let configService: typemoq.IMock<IConfigurationService>; + let extensions: typemoq.IMock<IExtensions>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let envVarsProvider: typemoq.IMock<IEnvironmentVariablesProvider>; + let onDidChangeRefreshState: EventEmitter<ProgressNotificationEvent>; + let onDidChangeEnvironments: EventEmitter<PythonEnvCollectionChangedEvent>; + let onDidChangeEnvironmentVariables: EventEmitter<Uri | undefined>; + + let environmentApi: PythonExtension['environments']; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getWorkspaceFolder').callsFake((resource: Resource) => { + if (resource?.fsPath === workspaceFolder.uri.fsPath) { + return workspaceFolder; + } + return undefined; + }); + discoverAPI = typemoq.Mock.ofType<IDiscoveryAPI>(); + extensions = typemoq.Mock.ofType<IExtensions>(); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + envVarsProvider = typemoq.Mock.ofType<IEnvironmentVariablesProvider>(); + extensions + .setup((e) => e.determineExtensionFromCallStack()) + .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })); + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + configService = typemoq.Mock.ofType<IConfigurationService>(); + onDidChangeRefreshState = new EventEmitter(); + onDidChangeEnvironments = new EventEmitter(); + onDidChangeEnvironmentVariables = new EventEmitter(); + serviceContainer.setup((s) => s.get(IExtensions)).returns(() => extensions.object); + serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IEnvironmentVariablesProvider)).returns(() => envVarsProvider.object); + envVarsProvider + .setup((e) => e.onDidEnvironmentVariablesChange) + .returns(() => onDidChangeEnvironmentVariables.event); + serviceContainer.setup((s) => s.get(IDisposableRegistry)).returns(() => []); + + discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); + discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event); + discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + const onDidChangePythonEnvironment = new EventEmitter<Uri>(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Provide an event to track when environment variables change', async () => { + const resource = workspaceFolder.uri; + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const events: EnvironmentVariablesChangeEvent[] = []; + environmentApi.onDidEnvironmentVariablesChange((e) => { + events.push(e); + }); + onDidChangeEnvironmentVariables.fire(resource); + await sleep(1); + assert.deepEqual(events, [{ env: envVars, resource: workspaceFolder }]); + }); + + test('getEnvironmentVariables: No resource', async () => { + const resource = undefined; + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const vars = environmentApi.getEnvironmentVariables(resource); + assert.deepEqual(vars, envVars); + }); + + test('getEnvironmentVariables: With Uri resource', async () => { + const resource = Uri.file('x'); + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const vars = environmentApi.getEnvironmentVariables(resource); + assert.deepEqual(vars, envVars); + }); + + test('getEnvironmentVariables: With WorkspaceFolder resource', async () => { + const resource = Uri.file('x'); + const folder = ({ uri: resource } as unknown) as WorkspaceFolder; + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const vars = environmentApi.getEnvironmentVariables(folder); + assert.deepEqual(vars, envVars); + }); + + test('Provide an event to track when active environment details change', async () => { + const events: ActiveEnvironmentPathChangeEvent[] = []; + environmentApi.onDidChangeActiveEnvironmentPath((e) => { + events.push(e); + }); + reportActiveInterpreterChanged({ path: 'path/to/environment', resource: undefined }); + await sleep(1); + assert.deepEqual(events, [ + { id: normCasePath('path/to/environment'), path: 'path/to/environment', resource: undefined }, + ]); + }); + + test('getActiveEnvironmentPath: No resource', () => { + const pythonPath = 'this/is/a/test/path'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = environmentApi.getActiveEnvironmentPath(); + assert.deepEqual(actual, { + id: normCasePath(pythonPath), + path: pythonPath, + }); + }); + + test('getActiveEnvironmentPath: default python', () => { + const pythonPath = 'python'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = environmentApi.getActiveEnvironmentPath(); + assert.deepEqual(actual, { + id: 'DEFAULT_PYTHON', + path: pythonPath, + }); + }); + + test('getActiveEnvironmentPath: With resource', () => { + const pythonPath = 'this/is/a/test/path'; + const resource = Uri.file(__filename); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = environmentApi.getActiveEnvironmentPath(resource); + assert.deepEqual(actual, { + id: normCasePath(pythonPath), + path: pythonPath, + }); + }); + + test('resolveEnvironment: invalid environment (when passed as string)', async () => { + const pythonPath = 'this/is/a/test/path'; + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(undefined)); + + const actual = await environmentApi.resolveEnvironment(pythonPath); + expect(actual).to.be.equal(undefined); + }); + + test('resolveEnvironment: valid environment (when passed as string)', async () => { + const pythonPath = 'this/is/a/test/path'; + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + + const actual = await environmentApi.resolveEnvironment(pythonPath); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); + + test('resolveEnvironment: valid environment (when passed as environment)', async () => { + const pythonPath = 'this/is/a/test/path'; + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }); + const partialEnv = buildEnvInfo({ + executable: pythonPath, + kind: PythonEnvKind.System, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + + const actual = await environmentApi.resolveEnvironment(convertCompleteEnvInfo(partialEnv)); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); + + test('environments: no pythons found', () => { + discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + const actual = environmentApi.known; + expect(actual).to.be.deep.equal([]); + }); + + test('environments: python found', async () => { + const expectedEnvs = [ + { + id: normCasePath('this/is/a/test/python/path1'), + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + { + id: normCasePath('this/is/a/test/python/path2'), + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: -1, + micro: -1, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + const envs = [ + ...expectedEnvs, + { + id: normCasePath('this/is/a/test/python/path3'), + executable: { + filename: 'this/is/a/test/python/path3', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: -1, + micro: -1, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + searchLocation: Uri.file('path/outside/workspace'), + }, + ]; + discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); + const onDidChangePythonEnvironment = new EventEmitter<Uri>(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); + const actual = environmentApi.known; + const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); + assert.deepEqual( + actualEnvs?.sort((a, b) => a.id.localeCompare(b.id)), + expectedEnvs.map((e) => convertEnvInfo(e)).sort((a, b) => a.id.localeCompare(b.id)), + ); + }); + + test('Provide an event to track when list of environments change', async () => { + let events: EnvironmentsChangeEvent[] = []; + let eventValues: EnvironmentsChangeEvent[] = []; + let expectedEvents: EnvironmentsChangeEvent[] = []; + environmentApi.onDidChangeEnvironments((e) => { + events.push(e); + }); + const envs = [ + buildEnvInfo({ + executable: 'pythonPath', + kind: PythonEnvKind.System, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }), + { + id: normCasePath('this/is/a/test/python/path1'), + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + { + id: normCasePath('this/is/a/test/python/path2'), + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 10, + micro: 0, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + + // Now fire and verify events. Note the event value holds the reference to an environment, so may itself + // change when the environment is altered. So it's important to verify them as soon as they're received. + + // Add events + onDidChangeEnvironments.fire({ old: undefined, new: envs[0] }); + expectedEvents.push({ env: convertEnvInfo(envs[0]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[1] }); + expectedEvents.push({ env: convertEnvInfo(envs[1]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[2] }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'add' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Update events + events = []; + expectedEvents = []; + const updatedEnv0 = cloneDeep(envs[0]); + updatedEnv0.arch = Architecture.x86; + onDidChangeEnvironments.fire({ old: envs[0], new: updatedEnv0 }); + expectedEvents.push({ env: convertEnvInfo(updatedEnv0), type: 'update' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Remove events + events = []; + expectedEvents = []; + onDidChangeEnvironments.fire({ old: envs[2], new: undefined }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'remove' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + const expectedEnvs = [convertEnvInfo(updatedEnv0), convertEnvInfo(envs[1])].sort(); + const knownEnvs = environmentApi.known.map((e) => (e as EnvironmentReference).internal).sort(); + + assert.deepEqual(expectedEnvs, knownEnvs); + }); + + test('updateActiveEnvironmentPath: no resource', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.updateActiveEnvironmentPath('this/is/a/test/python/path'); + + interpreterPathService.verifyAll(); + }); + + test('updateActiveEnvironmentPath: passed as Environment', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.updateActiveEnvironmentPath({ + id: normCasePath('this/is/a/test/python/path'), + path: 'this/is/a/test/python/path', + }); + + interpreterPathService.verifyAll(); + }); + + test('updateActiveEnvironmentPath: with uri', async () => { + const uri = Uri.parse('a'); + interpreterPathService + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.updateActiveEnvironmentPath('this/is/a/test/python/path', uri); + + interpreterPathService.verifyAll(); + }); + + test('updateActiveEnvironmentPath: with workspace folder', async () => { + const uri = Uri.parse('a'); + interpreterPathService + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + const workspace: WorkspaceFolder = { + uri, + name: '', + index: 0, + }; + + await environmentApi.updateActiveEnvironmentPath('this/is/a/test/python/path', workspace); + + interpreterPathService.verifyAll(); + }); + + test('refreshInterpreters: default', async () => { + discoverAPI + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: true }))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.refreshEnvironments(); + + discoverAPI.verifyAll(); + }); + + test('refreshInterpreters: when forcing a refresh', async () => { + discoverAPI + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: false }))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.refreshEnvironments({ forceRefresh: true }); + + discoverAPI.verifyAll(); + }); +}); diff --git a/src/test/extension-version.functional.test.ts b/src/test/extension-version.functional.test.ts index 7d2913b0f34b..c55c88c04d3b 100644 --- a/src/test/extension-version.functional.test.ts +++ b/src/test/extension-version.functional.test.ts @@ -3,8 +3,6 @@ 'use strict'; -// tslint:disable:no-any max-func-body-length - import { expect } from 'chai'; import * as fs from 'fs'; import * as glob from 'glob'; @@ -21,7 +19,6 @@ suite('Extension version tests', () => { suiteSetup(async function () { // Skip the entire suite if running locally if (!branchName) { - // tslint:disable-next-line: no-invalid-this return this.skip(); } }); @@ -31,27 +28,25 @@ suite('Extension version tests', () => { version = applicationEnvironment.packageJson.version; }); - test('If we are running a pipeline in the master branch, the extension version in `package.json` should have the "-dev" suffix', async function () { - if (branchName !== 'master') { - // tslint:disable-next-line: no-invalid-this + 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') { return this.skip(); } return expect( version.endsWith('-dev'), - 'When running a pipeline in the master branch, the extension version in package.json should have the -dev suffix' + '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' + 'When running a pipeline in the release branch, the extension version in package.json should not have the -dev suffix', ).to.be.false; }); }); diff --git a/src/test/extensionSettings.ts b/src/test/extensionSettings.ts new file mode 100644 index 000000000000..2d35dcb5f4ca --- /dev/null +++ b/src/test/extensionSettings.ts @@ -0,0 +1,56 @@ +/* eslint-disable global-require */ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Event, Uri } from 'vscode'; +import { IApplicationEnvironment } from '../client/common/application/types'; +import { WorkspaceService } from '../client/common/application/workspace'; +import { InterpreterPathService } from '../client/common/interpreterPathService'; +import { PersistentStateFactory } from '../client/common/persistentState'; +import { IPythonSettings, Resource } from '../client/common/types'; +import { PythonEnvironment } from '../client/pythonEnvironments/info'; +import { MockMemento } from './mocks/mementos'; +import { MockExtensions } from './mocks/extensions'; + +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 undefined; + } + + public async setWorkspaceInterpreter( + _resource: Uri, + _interpreter: PythonEnvironment | undefined, + ): Promise<void> { + return undefined; + } + } + const pythonSettings = require('../client/common/configSettings') as typeof import('../client/common/configSettings'); + const workspaceService = new WorkspaceService(); + const workspaceMemento = new MockMemento(); + const globalMemento = new MockMemento(); + const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); + const extensions = new MockExtensions(); + return pythonSettings.PythonSettings.getInstance( + resource, + new AutoSelectionService(), + workspaceService, + new InterpreterPathService(persistentStateFactory, workspaceService, [], { + remoteName: undefined, + } as IApplicationEnvironment), + undefined, + extensions, + ); +} diff --git a/src/test/fakeVSCFileSystemAPI.ts b/src/test/fakeVSCFileSystemAPI.ts new file mode 100644 index 000000000000..1811f51dcd04 --- /dev/null +++ b/src/test/fakeVSCFileSystemAPI.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { FileStat, FileType, Uri } from 'vscode'; +import * as fsextra from '../client/common/platform/fs-paths'; +import { convertStat } from '../client/common/platform/fileSystem'; +import { createDeferred } from '../client/common/utils/async'; + +/* eslint-disable class-methods-use-this */ + +// 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): 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); + } +} diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts index 0df2aaddffb8..fbd8c20c9659 100644 --- a/src/test/fixtures.ts +++ b/src/test/fixtures.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import { sleep } from '../client/common/utils/async'; import { PYTHON_PATH } from './common'; import { Proc, spawn } from './proc'; @@ -39,12 +39,11 @@ export class CleanupFixture { 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...'); } - }) + }), ); } } @@ -53,7 +52,7 @@ export class PythonFixture extends CleanupFixture { public readonly python: string; constructor( // If not provided, we will use the global default. - python?: string + python?: string, ) { super(); if (python) { diff --git a/src/test/format/extension.dispatch.test.ts b/src/test/format/extension.dispatch.test.ts deleted file mode 100644 index 19d937a4cfc6..000000000000 --- a/src/test/format/extension.dispatch.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -// 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 deleted file mode 100644 index 631c47bfeea6..000000000000 --- a/src/test/format/extension.format.ds.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -// 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 deleted file mode 100644 index 8588f9ff26c5..000000000000 --- a/src/test/format/extension.format.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -// 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(); - - registerForIOC(ioc.serviceManager); - - // Mocks. - ioc.registerMockProcessTypes(); - ioc.registerMockInterpreterTypes(); - } - - 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 deleted file mode 100644 index 37c6b33edc93..000000000000 --- a/src/test/format/extension.lineFormatter.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -// 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 deleted file mode 100644 index 69bf32e47366..000000000000 --- a/src/test/format/extension.onEnterFormat.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -// 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 deleted file mode 100644 index 767d378db80d..000000000000 --- a/src/test/format/extension.onTypeFormat.test.ts +++ /dev/null @@ -1,790 +0,0 @@ -// 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 deleted file mode 100644 index f608ca404e85..000000000000 --- a/src/test/format/extension.sort.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -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(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]; - 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.base import libutils${EOL}from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}from rope.refactor.rename import Rename${EOL}` && - value.range.isEqual(new Range(6, 0, 6, 0)) - ).length, - 1, - 'Text not found' - ); - assert.equal( - edits.filter((value) => value.newText === '' && value.range.isEqual(new Range(13, 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 deleted file mode 100644 index 9ec60c994cb8..000000000000 --- a/src/test/format/format.helper.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -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 deleted file mode 100644 index 679b0d06f093..000000000000 --- a/src/test/format/formatter.unit.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -// 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 index d8d5a7324540..a4c69a2a9ac6 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -3,7 +3,6 @@ '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'); @@ -15,13 +14,7 @@ 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 { IS_MULTI_ROOT_TEST, MAX_EXTENSION_ACTIVATION_TIME, TEST_RETRYCOUNT, TEST_TIMEOUT } from './constants'; import { initialize } from './initialize'; import { initializeLogger } from './testLogger'; @@ -45,14 +38,12 @@ process.on('unhandledRejection', (ex: any, _a) => { 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'; @@ -71,7 +62,6 @@ function configure(): SetupOptions { const options: SetupOptions & { retries: number; invert: boolean } = { ui: 'tdd', - useColors: true, invert, timeout: TEST_TIMEOUT, retries: TEST_RETRYCOUNT, @@ -79,7 +69,7 @@ function configure(): SetupOptions { 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 + exit: true, }; // If the `MOCHA_REPORTER_JUNIT` env var is true, set up the CI reporter for @@ -90,7 +80,7 @@ function configure(): SetupOptions { options.reporter = 'mocha-multi-reporters'; const reporterPath = path.join(__dirname, 'common', 'exitCIAfterTestReporter.js'); options.reporterOptions = { - reporterEnabled: `spec,mocha-junit-reporter,${reporterPath}` + reporterEnabled: `spec,mocha-junit-reporter,${reporterPath}`, }; } @@ -111,17 +101,16 @@ function configure(): SetupOptions { * 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; + let timer: NodeJS.Timeout | 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; } @@ -129,29 +118,19 @@ function activatePythonExtensionScript() { /** * 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 mocha = new Mocha.default(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( + glob.default( `**/**.${options.testFilesSuffix}.js`, { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'].concat(ignoreGlob), cwd: testsRoot }, (error, files) => { @@ -159,14 +138,13 @@ export async function run(): Promise<void> { 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(); diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 6f19dbc4a8d1..0ed75a0aa5c1 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,14 +1,12 @@ -// tslint:disable:no-string-literal - import * as path from 'path'; import * as vscode from 'vscode'; -import type { IExtensionApi } from '../client/api'; +import type { PythonExtension } from '../client/api/types'; import { clearPythonPathInWorkspaceFolder, IExtensionTestApi, PYTHON_PATH, resetGlobalPythonPathSetting, - setPythonPathInWorkspaceRoot + setPythonPathInWorkspaceRoot, } from './common'; import { IS_SMOKE_TEST, PVSC_EXTENSION_ID_FOR_TESTS } from './constants'; import { sleep } from './core'; @@ -16,7 +14,7 @@ import { sleep } from './core'; export * from './constants'; export * from './ciConstants'; -const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'python_files', 'dummy.py'); export const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc'); const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3')); @@ -31,33 +29,36 @@ export async function initializePython() { await setPythonPathInWorkspaceRoot(PYTHON_PATH); } -// tslint:disable-next-line:no-any export async function initialize(): Promise<IExtensionTestApi> { await initializePython(); + + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('experiments.optInto', ['All'], vscode.ConfigurationTarget.Global); + await pythonConfig.update('experiments.optOutFrom', [], vscode.ConfigurationTarget.Global); 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'); + const configSettings = await import('../client/common/configSettings.js'); // 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 extension = vscode.extensions.getExtension<PythonExtension>(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'); + const configSettings = await import('../client/common/configSettings.js'); // Dispose any cached python settings (used only in test env). configSettings.PythonSettings.dispose(); } @@ -71,8 +72,8 @@ export async function closeActiveNotebooks(): Promise<void> { 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) { + + while ((vscode as any).window.activeNotebookEditor || vscode.window.activeTextEditor) { await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); } // Work around VS Code issues (sometimes notebooks do not get closed). @@ -98,18 +99,15 @@ async function closeWindowsInteral() { (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; + if (!vscode.window.activeTextEditor?.document) { + return false; } - return !!(vscode as any).notebook.activeNotebookEditor; + + return !!(vscode.window.activeTextEditor.document as any).notebook; } diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts index fd96f8ea663e..e43fa21daf17 100644 --- a/src/test/install/channelManager.channels.test.ts +++ b/src/test/install/channelManager.channels.test.ts @@ -3,60 +3,36 @@ 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 + IInterpreterAutoSelectionProxyService, } 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 { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info'; import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { createTypeMoq } from '../mocks/helper'; -const info: PythonInterpreter = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - type: InterpreterType.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 + MockAutoSelectionService, ); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( - IInterpreterAutoSeletionProxyService, - MockAutoSelectionService + serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, ); }); @@ -64,8 +40,8 @@ suite('Installation - installation channels', () => { 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'); + assert.strictEqual(channels.length, 1, 'Incorrect number of channels'); + assert.strictEqual(channels[0], installer.object, 'Incorrect installer'); }); test('Multiple channels', async () => { @@ -75,9 +51,9 @@ suite('Installation - installation channels', () => { 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'); + assert.strictEqual(channels.length, 2, 'Incorrect number of channels'); + assert.strictEqual(channels[0], installer1.object, 'Incorrect installer 1'); + assert.strictEqual(channels[1], installer3.object, 'Incorrect installer 2'); }); test('pipenv channel', async () => { @@ -86,57 +62,50 @@ suite('Installation - installation channels', () => { mockInstaller(true, '3'); const pipenvInstaller = mockInstaller(true, 'pipenv', 10); - const interpreter: PythonInterpreter = { - ...info, - path: 'pipenv', - type: InterpreterType.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'); + assert.strictEqual(channels.length, 1, 'Incorrect number of channels'); + assert.strictEqual(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>(); + const appShell = createTypeMoq<IApplicationShell>(); serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); - // tslint:disable-next-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any let items: any[] | undefined; appShell .setup((x) => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((i: string[], _o: QuickPickOptions) => { + .callback((i: string[]) => { items = i; }) .returns( - () => new Promise<string | undefined>((resolve, _reject) => resolve(undefined)) + () => 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); + await cm.getInstallationChannel(Product.pytest); - 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'); + assert.notStrictEqual(items, undefined, 'showQuickPick not called'); + assert.strictEqual(items!.length, 2, 'Incorrect number of installer shown'); + assert.notStrictEqual(items![0]!.label!.indexOf('Name 1'), -1, 'Incorrect first installer name'); + assert.notStrictEqual(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>(); + const installer = createTypeMoq<IModuleInstaller>(); installer .setup((x) => x.isSupported(TypeMoq.It.isAny())) .returns( - () => new Promise<boolean>((resolve) => resolve(supported)) + () => new Promise<boolean>((resolve) => resolve(supported)), ); - installer.setup((x) => x.priority).returns(() => (priority ? priority : 0)); + installer.setup((x) => x.priority).returns(() => 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 index 94b45ef2a17c..1e9953b8b753 100644 --- a/src/test/install/channelManager.messages.test.ts +++ b/src/test/install/channelManager.messages.test.ts @@ -13,28 +13,28 @@ import { Product } from '../../client/common/types'; import { Architecture } from '../../client/common/utils/platform'; import { IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService + IInterpreterAutoSelectionProxyService, } 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 { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { createTypeMoq } from '../mocks/helper'; -const info: PythonInterpreter = { +const info: PythonEnvironment = { architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: '', - type: InterpreterType.Unknown, + envType: EnvironmentType.Unknown, version: new SemVer('0.0.0-alpha'), sysPrefix: '', - sysVersion: '' + sysVersion: '', }; -// tslint:disable-next-line:max-func-body-length suite('Installation - channel messages', () => { let serviceContainer: IServiceContainer; let platform: TypeMoq.IMock<IPlatformService>; @@ -46,30 +46,30 @@ suite('Installation - channel messages', () => { const serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - platform = TypeMoq.Mock.ofType<IPlatformService>(); + platform = createTypeMoq<IPlatformService>(); serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, platform.object); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell = createTypeMoq<IApplicationShell>(); serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); - interpreters = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreters = createTypeMoq<IInterpreterService>(); serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, interpreters.object); - const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + const moduleInstaller = createTypeMoq<IModuleInstaller>(); serviceManager.addSingletonInstance<IModuleInstaller>(IModuleInstaller, moduleInstaller.object); serviceManager.addSingleton<IInterpreterAutoSelectionService>( IInterpreterAutoSelectionService, - MockAutoSelectionService + MockAutoSelectionService, ); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( - IInterpreterAutoSeletionProxyService, - MockAutoSelectionService + serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, ); }); test('No installers message: Unknown/Windows', async () => { platform.setup((x) => x.isWindows).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Unknown, async (message: string, url: string) => { + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Windows', 'Pip']); }); @@ -77,7 +77,7 @@ suite('Installation - channel messages', () => { test('No installers message: Conda/Windows', async () => { platform.setup((x) => x.isWindows).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Conda, async (message: string, url: string) => { + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { verifyMessage(message, ['Pip', 'Conda'], []); verifyUrl(url, ['Windows', 'Pip', 'Conda']); }); @@ -86,7 +86,7 @@ suite('Installation - channel messages', () => { test('No installers message: Unknown/Mac', async () => { platform.setup((x) => x.isWindows).returns(() => false); platform.setup((x) => x.isMac).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Unknown, async (message: string, url: string) => { + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Mac', 'Pip']); }); @@ -95,7 +95,7 @@ suite('Installation - channel messages', () => { test('No installers message: Conda/Mac', async () => { platform.setup((x) => x.isWindows).returns(() => false); platform.setup((x) => x.isMac).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Conda, async (message: string, url: string) => { + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { verifyMessage(message, ['Pip', 'Conda'], []); verifyUrl(url, ['Mac', 'Pip', 'Conda']); }); @@ -105,7 +105,7 @@ suite('Installation - channel messages', () => { platform.setup((x) => x.isWindows).returns(() => false); platform.setup((x) => x.isMac).returns(() => false); platform.setup((x) => x.isLinux).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Unknown, async (message: string, url: string) => { + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Linux', 'Pip']); }); @@ -115,7 +115,7 @@ suite('Installation - channel messages', () => { platform.setup((x) => x.isWindows).returns(() => false); platform.setup((x) => x.isMac).returns(() => false); platform.setup((x) => x.isLinux).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Conda, async (message: string, url: string) => { + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { verifyMessage(message, ['Pip', 'Conda'], []); verifyUrl(url, ['Linux', 'Pip', 'Conda']); }); @@ -124,51 +124,51 @@ suite('Installation - channel messages', () => { test('No channels message', async () => { platform.setup((x) => x.isWindows).returns(() => true); await testInstallerMissingMessage( - InterpreterType.Unknown, + EnvironmentType.Unknown, async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Windows', 'Pip']); }, - 'getInstallationChannel' + '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}.`); + assert.strictEqual(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}.`); + assert.strictEqual(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.'); + assert.strictEqual(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}.`); + assert.strictEqual(url.indexOf(term) >= 0, true, `Search Url does not contain ${term}.`); } } async function testInstallerMissingMessage( - interpreterType: InterpreterType, + interpreterType: EnvironmentType, verify: (m: string, u: string) => Promise<void>, - methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage' + methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage', ): Promise<void> { - const activeInterpreter: PythonInterpreter = { + const activeInterpreter: PythonEnvironment = { ...info, - type: interpreterType, - path: '' + envType: interpreterType, + path: '', }; interpreters .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) .returns( - () => new Promise<PythonInterpreter>((resolve, _reject) => resolve(activeInterpreter)) + () => new Promise<PythonEnvironment>((resolve, _reject) => resolve(activeInterpreter)), ); const channels = new InstallationChannelManager(serviceContainer); - let url: string = ''; - let message: string = ''; - let search: string = ''; + let url = ''; + let message = ''; + let search = ''; appShell .setup((x) => x.showErrorMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) .callback((m: string, s: string) => { @@ -176,7 +176,7 @@ suite('Installation - channel messages', () => { search = s; }) .returns( - () => new Promise<string>((resolve, _reject) => resolve(search)) + () => new Promise<string>((resolve, _reject) => resolve(search)), ); appShell .setup((x) => x.openUrl(TypeMoq.It.isAnyString())) @@ -186,7 +186,7 @@ suite('Installation - channel messages', () => { if (methodType === 'showNoInstallersMessage') { await channels.showNoInstallersMessage(); } else { - await channels.getInstallationChannel(Product.pylint); + await channels.getInstallationChannel(Product.pytest); } await verify(message, url); } diff --git a/src/test/interpreters/activation/indicatorPrompt.unit.test.ts b/src/test/interpreters/activation/indicatorPrompt.unit.test.ts new file mode 100644 index 000000000000..b15cd84dc01a --- /dev/null +++ b/src/test/interpreters/activation/indicatorPrompt.unit.test.ts @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; +import { + IConfigurationService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, +} from '../../../client/common/types'; +import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import * as extapi from '../../../client/envExt/api.internal'; + +suite('Terminal Activation Indicator Prompt', () => { + let shell: IApplicationShell; + let terminalManager: ITerminalManager; + let experimentService: IExperimentService; + let activeResourceService: IActiveResourceService; + let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; + let persistentStateFactory: IPersistentStateFactory; + let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; + let terminalEventEmitter: EventEmitter<Terminal>; + let notificationEnabled: IPersistentState<boolean>; + let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; + let useEnvExtensionStub: sinon.SinonStub; + const prompts = [Common.doNotShowAgain]; + const envName = 'env'; + const type = PythonEnvType.Virtual; + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format('Python virtual', `"(${envName})"`); + + setup(async () => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + shell = mock<IApplicationShell>(); + terminalManager = mock<ITerminalManager>(); + interpreterService = mock<IInterpreterService>(); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName, + type, + } as unknown) as PythonEnvironment); + experimentService = mock<IExperimentService>(); + activeResourceService = mock<IActiveResourceService>(); + persistentStateFactory = mock<IPersistentStateFactory>(); + terminalEnvVarCollectionService = mock<ITerminalEnvVarCollectionService>(); + configurationService = mock<IConfigurationService>(); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: true, + }, + } as unknown) as IPythonSettings); + notificationEnabled = mock<IPersistentState<boolean>>(); + terminalEventEmitter = new EventEmitter<Terminal>(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); + terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( + instance(shell), + instance(persistentStateFactory), + instance(terminalManager), + [], + instance(activeResourceService), + instance(terminalEnvVarCollectionService), + instance(configurationService), + instance(interpreterService), + instance(experimentService), + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Show notification when a new terminal is opened for which there is no prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).once(); + }); + + test('Do not show notification if automatic terminal activation is turned off', async () => { + reset(configurationService); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: false, + }, + } as unknown) as IPythonSettings); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(false); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn( + Promise.resolve(Common.doNotShowAgain), + ); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Do not disable notification if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(undefined)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).never(); + }); +}); diff --git a/src/test/interpreters/activation/preWarmVariables.unit.test.ts b/src/test/interpreters/activation/preWarmVariables.unit.test.ts deleted file mode 100644 index 0881b5216e1e..000000000000 --- a/src/test/interpreters/activation/preWarmVariables.unit.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -// 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 index d44ed185dc4c..a0f9b3bd6915 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -18,7 +18,7 @@ import { ProcessServiceFactory } from '../../../client/common/process/processFac 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 { ICurrentProcess, Resource } 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'; @@ -27,17 +27,17 @@ 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 { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const defaultShells = { [OSType.Windows]: 'cmd', [OSType.OSX]: 'bash', [OSType.Linux]: 'bash', - [OSType.Unknown]: undefined + [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; @@ -49,17 +49,17 @@ suite('Interpreters Activation - Python Environment Variables', () => { let workspace: IWorkspaceService; let interpreterService: IInterpreterService; let onDidChangeEnvVariables: EventEmitter<Uri | undefined>; - let onDidChangeInterpreter: EventEmitter<void>; - const pythonInterpreter: PythonInterpreter = { + let onDidChangeInterpreter: EventEmitter<Resource>; + const pythonInterpreter: PythonEnvironment = { path: '/foo/bar/python.exe', version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, }; - function initSetup() { + function initSetup(interpreter: PythonEnvironment | undefined) { helper = mock(TerminalHelper); platform = mock(PlatformService); processServiceFactory = mock(ProcessServiceFactory); @@ -69,9 +69,10 @@ suite('Interpreters Activation - Python Environment Variables', () => { interpreterService = mock(InterpreterService); workspace = mock(WorkspaceService); onDidChangeEnvVariables = new EventEmitter<Uri | undefined>(); - onDidChangeInterpreter = new EventEmitter<void>(); + onDidChangeInterpreter = new EventEmitter<Resource>(); when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event); when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); service = new EnvironmentActivationService( instance(helper), instance(platform), @@ -79,18 +80,18 @@ suite('Interpreters Activation - Python Environment Variables', () => { instance(currentProcess), instance(workspace), instance(interpreterService), - instance(envVarsService) + instance(envVarsService), ); } - function title(resource?: Uri, interpreter?: PythonInterpreter) { + 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); + setup(() => initSetup(interpreter)); test('Unknown os will return empty variables', async () => { when(platform.osType).thenReturn(OSType.Unknown); const env = await service.getActivatedEnvironmentVariables(resource); @@ -103,11 +104,11 @@ suite('Interpreters Activation - Python Environment Variables', () => { osTypes.forEach((osType) => { suite(osType.name, () => { - setup(initSetup); + setup(() => initSetup(interpreter)); test('getEnvironmentActivationShellCommands will be invoked', async () => { when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(); const env = await service.getActivatedEnvironmentVariables(resource, interpreter); @@ -115,7 +116,26 @@ suite('Interpreters Activation - Python Environment Variables', () => { verify(platform.osType).once(); expect(env).to.equal(undefined, 'Should not have any variables'); verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + }); + test('Env variables returned for microvenv', async () => { + when(platform.osType).thenReturn(osType.value); + + const microVenv = { ...pythonInterpreter, envType: EnvironmentType.Venv }; + const key = getSearchPathEnvVarNames()[0]; + const varsFromEnv = { [key]: '/foo/bar' }; + + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), microVenv), + ).thenResolve(); + + const env = await service.getActivatedEnvironmentVariables(resource, microVenv); + + verify(platform.osType).once(); + expect(env).to.deep.equal(varsFromEnv); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), microVenv), ).once(); }); test('Validate command used to activation and printing env vars', async () => { @@ -123,7 +143,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const envVars = { one: '1', two: '2' }; when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(cmd); when(processServiceFactory.create(resource)).thenResolve(instance(processService)); when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); @@ -133,7 +153,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { verify(platform.osType).once(); expect(env).to.equal(undefined, 'Should not have any variables'); verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).once(); verify(processServiceFactory.create(resource)).once(); verify(envVarsService.getEnvironmentVariables(resource)).once(); @@ -141,12 +161,15 @@ suite('Interpreters Activation - Python Environment Variables', () => { 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 printEnvPyFile = path.join( + EXTENSION_ROOT_DIR, + 'python_files', + 'printEnvVariables.py', + ); const expectedCommand = [ ...cmd, `echo '${getEnvironmentPrefix}'`, - `python ${isolated} ${printEnvPyFile.fileToCommandArgument()}` + `python ${printEnvPyFile.fileToCommandArgumentForPythonExt()}`, ].join(' && '); expect(shellCmd).to.equal(expectedCommand); @@ -156,7 +179,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const envVars = { one: '1', two: '2' }; when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(cmd); when(processServiceFactory.create(resource)).thenResolve(instance(processService)); when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); @@ -166,7 +189,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { verify(platform.osType).once(); expect(env).to.equal(undefined, 'Should not have any variables'); verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).once(); verify(processServiceFactory.create(resource)).once(); verify(envVarsService.getEnvironmentVariables(resource)).once(); @@ -175,13 +198,13 @@ suite('Interpreters Activation - Python Environment Variables', () => { 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 + throwOnStdErr: false, }); }); test('Use current process variables if there are no custom variables', async () => { @@ -189,7 +212,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const envVars = { one: '1', two: '2', PYTHONWARNINGS: 'ignore' }; when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(cmd); when(processServiceFactory.create(resource)).thenResolve(instance(processService)); when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); @@ -200,7 +223,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { verify(platform.osType).once(); expect(env).to.equal(undefined, 'Should not have any variables'); verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).once(); verify(processServiceFactory.create(resource)).once(); verify(envVarsService.getEnvironmentVariables(resource)).once(); @@ -210,13 +233,13 @@ suite('Interpreters Activation - Python Environment Variables', () => { 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 + throwOnStdErr: false, }); }); test('Error must be swallowed when activation fails', async () => { @@ -224,7 +247,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const envVars = { one: '1', two: '2' }; when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(cmd); when(processServiceFactory.create(resource)).thenResolve(instance(processService)); when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); @@ -235,7 +258,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { verify(platform.osType).once(); expect(env).to.equal(undefined, 'Should not have any variables'); verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).once(); verify(processServiceFactory.create(resource)).once(); verify(envVarsService.getEnvironmentVariables(resource)).once(); @@ -248,7 +271,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(cmd); when(processServiceFactory.create(resource)).thenResolve(instance(processService)); when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); @@ -259,7 +282,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { verify(platform.osType).once(); expect(env).to.deep.equal(varsFromEnv); verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).once(); verify(processServiceFactory.create(resource)).once(); verify(envVarsService.getEnvironmentVariables(resource)).once(); @@ -271,7 +294,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(cmd); when(processServiceFactory.create(resource)).thenResolve(instance(processService)); when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); @@ -287,7 +310,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { // All methods invoked only once. verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).once(); verify(processServiceFactory.create(resource)).once(); verify(envVarsService.getEnvironmentVariables(resource)).once(); @@ -299,7 +322,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; when(platform.osType).thenReturn(osType.value); when( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).thenResolve(cmd); when(processServiceFactory.create(resource)).thenResolve(instance(processService)); when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); @@ -317,21 +340,18 @@ suite('Interpreters Activation - Python Environment Variables', () => { // All methods invoked twice as cache was blown. verify( - helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + 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/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts new file mode 100644 index 000000000000..dfe3ad8c081a --- /dev/null +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -0,0 +1,773 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import { assert, expect } from 'chai'; +import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { + EnvironmentVariableCollection, + EnvironmentVariableMutatorOptions, + GlobalEnvironmentVariableCollection, + ProgressLocation, + Uri, + WorkspaceConfiguration, + WorkspaceFolder, +} from 'vscode'; +import { + IApplicationShell, + IApplicationEnvironment, + IWorkspaceService, +} from '../../../client/common/application/types'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { + IExtensionContext, + IExperimentService, + Resource, + IConfigurationService, + IPythonSettings, +} from '../../../client/common/types'; +import { Interpreters } from '../../../client/common/utils/localize'; +import { OSType, getOSType } from '../../../client/common/utils/platform'; +import { defaultShells } from '../../../client/interpreter/activation/service'; +import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { IShellIntegrationDetectionService, ITerminalDeactivateService } from '../../../client/terminals/types'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import * as extapi from '../../../client/envExt/api.internal'; + +suite('Terminal Environment Variable Collection Service', () => { + let platform: IPlatformService; + let interpreterService: IInterpreterService; + let context: IExtensionContext; + let shell: IApplicationShell; + let experimentService: IExperimentService; + let collection: EnvironmentVariableCollection; + let globalCollection: GlobalEnvironmentVariableCollection; + let applicationEnvironment: IApplicationEnvironment; + let environmentActivationService: IEnvironmentActivationService; + let workspaceService: IWorkspaceService; + let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; + let terminalDeactivateService: ITerminalDeactivateService; + let useEnvExtensionStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock<WorkspaceConfiguration>; + const progressOptions = { + location: ProgressLocation.Window, + title: Interpreters.activatingTerminals, + }; + let configService: IConfigurationService; + let shellIntegrationService: IShellIntegrationDetectionService; + const displayPath = 'display/path'; + const customShell = 'powershell'; + const defaultShell = defaultShells[getOSType()]; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + workspaceService = mock<IWorkspaceService>(); + terminalDeactivateService = mock<ITerminalDeactivateService>(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve(undefined); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + when(workspaceService.workspaceFolders).thenReturn(undefined); + platform = mock<IPlatformService>(); + when(platform.osType).thenReturn(getOSType()); + interpreterService = mock<IInterpreterService>(); + context = mock<IExtensionContext>(); + shell = mock<IApplicationShell>(); + const envVarProvider = mock<IEnvironmentVariablesProvider>(); + shellIntegrationService = mock<IShellIntegrationDetectionService>(); + when(shellIntegrationService.isWorking()).thenResolve(true); + globalCollection = mock<GlobalEnvironmentVariableCollection>(); + collection = mock<EnvironmentVariableCollection>(); + when(context.environmentVariableCollection).thenReturn(instance(globalCollection)); + when(globalCollection.getScoped(anything())).thenReturn(instance(collection)); + experimentService = mock<IExperimentService>(); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + applicationEnvironment = mock<IApplicationEnvironment>(); + when(applicationEnvironment.shell).thenReturn(customShell); + when(shell.withProgress(anything(), anything())) + .thenCall((options, _) => { + expect(options).to.deep.equal(progressOptions); + }) + .thenResolve(); + environmentActivationService = mock<IEnvironmentActivationService>(); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + process.env, + ); + configService = mock<IConfigurationService>(); + when(configService.getSettings(anything())).thenReturn(({ + terminal: { activateEnvironment: true }, + pythonPath: displayPath, + } as unknown) as IPythonSettings); + when(collection.clear()).thenResolve(); + terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( + instance(platform), + instance(interpreterService), + instance(context), + instance(shell), + instance(experimentService), + instance(applicationEnvironment), + [], + instance(environmentActivationService), + instance(workspaceService), + instance(configService), + instance(terminalDeactivateService), + new PathUtils(getOSType() === OSType.Windows), + instance(shellIntegrationService), + instance(envVarProvider), + ); + pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Apply activated variables to the collection on activation', async () => { + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); + await terminalEnvVarCollectionService.activate(undefined); + assert(applyCollectionStub.calledOnce, 'Collection not applied on activation'); + }); + + test('When not in experiment, do not apply activated variables to the collection and clear it instead', async () => { + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService.activate(undefined); + + verify(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).once(); + verify(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).never(); + assert(applyCollectionStub.notCalled, 'Collection should not be applied on activation'); + + verify(globalCollection.clear()).atLeast(1); + }); + + test('When interpreter changes, apply new activated variables to the collection', async () => { + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + const resource = Uri.file('x'); + let callback: (resource: Resource) => Promise<void>; + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenCall((cb) => { + callback = cb; + }); + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); + await terminalEnvVarCollectionService.activate(undefined); + + await callback!(resource); + assert(applyCollectionStub.calledWithExactly(resource)); + }); + + test('When selected shell changes, apply new activated variables to the collection', async () => { + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + let callback: (shell: string) => Promise<void>; + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenCall((cb) => { + callback = cb; + }); + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); + await terminalEnvVarCollectionService.activate(undefined); + + await callback!(customShell); + assert(applyCollectionStub.calledWithExactly(undefined, customShell)); + }); + + test('If activated variables are returned for custom shell, apply it correctly to the collection', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + }); + + // eslint-disable-next-line consistent-return + test('If activated variables contain PS1, prefix it using shell integration', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + PS1: '(envName) extra prompt', // Should not use this + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName: 'envName', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + + test('Respect VIRTUAL_ENV_DISABLE_PROMPT when setting PS1 for venv', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + VIRTUAL_ENV_DISABLE_PROMPT: '1', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', anything(), anything())).never(); + }); + + test('Otherwise set PS1 for venv even if PS1 is not returned', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', '(envName) ', anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', '(envName) ', anything())).once(); + }); + + test('Respect CONDA_PROMPT_MODIFIER when setting PS1 for conda', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(envName)', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + + test('Prepend only "prepend portion of PATH" where applicable', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', prependedPart, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Also prepend deactivate script location if available', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenReject(); // Verify we swallow errors from here + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + verify(collection.prepend('PATH', `scriptLocation${separator}${prependedPart}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH with separator otherwise', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH with separator otherwise', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `scriptLocation${separator}${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Verify envs are not applied if env activation is disabled', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + reset(configService); + when(configService.getSettings(anything())).thenReturn(({ + terminal: { activateEnvironment: false }, + pythonPath: displayPath, + } as unknown) as IPythonSettings); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); + }); + + test('Verify correct options are used when applying envs and setting description', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenCall( + (_e, _v, options: EnvironmentVariableMutatorOptions) => { + assert.deepEqual(options, { applyAtShellIntegration: true, applyAtProcessCreation: true }); + return Promise.resolve(); + }, + ); + + await terminalEnvVarCollectionService._applyCollection(resource, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + }); + + test('Correct track that prompt was set for non-Windows bash where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for PS1 if shell integration is disabled', async () => { + reset(shellIntegrationService); + when(shellIntegrationService.isWorking()).thenResolve(false); + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set for non-Windows where PS1 is not set but should be set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(base)', + }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'base', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was not set for non-Windows fish where PS1 is not set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'fish'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set correctly for global interpreters', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: undefined, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(undefined); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for Windows when not using powershell', async () => { + when(platform.osType).thenReturn(OSType.Windows); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'cmd'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for Windows when using powershell', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'powershell'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(undefined); + const envVars = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + defaultShell?.shell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(collection.clear()).once(); + }); + + test('If no activated variables are returned for default shell, clear collection', async () => { + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + defaultShell?.shell, + ), + ).thenResolve(undefined); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, defaultShell?.shell); + + verify(collection.clear()).once(); + }); +}); diff --git a/src/test/interpreters/activation/terminalEnvironmentActivationService.unit.test.ts b/src/test/interpreters/activation/terminalEnvironmentActivationService.unit.test.ts deleted file mode 100644 index e3617798c09d..000000000000 --- a/src/test/interpreters/activation/terminalEnvironmentActivationService.unit.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - architecture: Architecture.Unknown, - path: '', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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 deleted file mode 100644 index 7e2ef4443057..000000000000 --- a/src/test/interpreters/activation/wrapperEnvironmentActivationService.unit.test.ts +++ /dev/null @@ -1,328 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - architecture: Architecture.Unknown, - path: '', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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: PythonInterpreter = { - architecture: Architecture.x64, - path: 'New', - sysPrefix: '', - sysVersion: '', - type: InterpreterType.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 index 21fbdcc83315..6c5473546614 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -3,36 +3,32 @@ '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, instance, mock, reset, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; +import { anyString, anything, instance, mock, 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 { IExperimentService, 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 { InterpreterAutoSelectionProxyService } from '../../../client/interpreter/autoSelection/proxy'; +import { IInterpreterAutoSelectionProxyService } from '../../../client/interpreter/autoSelection/types'; +import { EnvironmentTypeComparer } from '../../../client/interpreter/configuration/environmentTypeComparer'; +import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; -import { PythonInterpreter } from '../../../client/pythonEnvironments/info'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as Telemetry from '../../../client/telemetry'; +import { EventName } from '../../../client/telemetry/constants'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; @@ -41,179 +37,470 @@ suite('Interpreters - Auto Selection', () => { 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<PythonInterpreter | undefined>; + let state: PersistentState<PythonEnvironment | undefined>; let helper: IInterpreterHelper; - let proxy: IInterpreterAutoSeletionProxyService; - let interpreterSecurityService: IInterpreterSecurityService; + let proxy: IInterpreterAutoSelectionProxyService; + let interpreterService: IInterpreterService; + let experimentService: IExperimentService; + let sendTelemetryEventStub: sinon.SinonStub; + let telemetryEvents: { eventName: string; properties: Record<string, unknown> }[] = []; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { public initializeStore(resource: Resource): Promise<void> { return super.initializeStore(resource); } - public storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { + + 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); + state = mock(PersistentState) as PersistentState<PythonEnvironment | undefined>; 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); + proxy = mock(InterpreterAutoSelectionProxyService); + interpreterService = mock(InterpreterService); + experimentService = mock<IExperimentService>(); + when(experimentService.inExperimentSync(anything())).thenReturn(false); + + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); autoSelectionService = new InterpreterAutoSelectionServiceTest( instance(workspaceService), instance(stateFactory), instance(fs), - instance(systemInterpreter), - instance(currentPathInterpreter), - instance(winRegInterpreter), - instance(cachedPaths), - instance(userDefinedInterpreter), - instance(workspaceInterpreter), + instance(interpreterService), + interpreterComparer, instance(proxy), instance(helper), - instance(interpreterSecurityService) + instance(experimentService), ); + + when(interpreterService.refreshPromise).thenReturn(undefined); + when(interpreterService.getInterpreters(anything())).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pyenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 5, patch: 0 }, + } as PythonEnvironment, + ]); + + sendTelemetryEventStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake((( + eventName: string, + _, + properties: Record<string, unknown>, + ) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }) as typeof Telemetry.sendTelemetryEvent); + }); + + teardown(() => { + sinon.restore(); + Telemetry._resetSharedProperties(); + telemetryEvents = []; }); 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(); + suite('Test locator-based auto-selection method', () => { + let workspacePath: string; + let resource: Uri; + let eventFired: boolean; + + setup(() => { + workspacePath = path.join('path', 'to', 'workspace'); + resource = Uri.parse('resource'); + eventFired = false; + + const folderUri = { fsPath: workspacePath }; + + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ + folderUri, + } as WorkspacePythonPath); + when( + stateFactory.createWorkspacePersistentState<PythonEnvironment | undefined>(anyString(), undefined), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + 'autoSelectionInterpretersQueriedOnce', + undefined, + ), + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), '')).thenReturn('workspaceIdentifier'); + + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); + }); + + test('If there is a local environment select it', async () => { + const localEnv = { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment, + localEnv, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(localEnv)).once(); + }); + + test('If there are no local environments, return a globally-installed interpreter', async () => { + const systemEnv = { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + systemEnv, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(systemEnv)).once(); + }); + + test('getInterpreters is called with ignoreCache at true if there is no value set in the workspace persistent state', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + + const globalQueriedState = mock(PersistentState) as PersistentState<boolean | undefined>; + when(globalQueriedState.value).thenReturn(true); + when(stateFactory.createGlobalPersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(globalQueriedState), + ); + + const queryState = mock(PersistentState) as PersistentState<boolean | undefined>; + + when(queryState.value).thenReturn(undefined); + when(stateFactory.createWorkspacePersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + when(interpreterService.triggerRefresh(anything())).thenResolve(); + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.triggerRefresh(anything())).once(); + }); + + test('getInterpreters is called with ignoreCache at false if there is a value set in the workspace persistent state', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + const queryState = mock(PersistentState) as PersistentState<boolean | undefined>; + + when(queryState.value).thenReturn(true); + when(stateFactory.createWorkspacePersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + when(interpreterService.triggerRefresh(anything())).thenResolve(); + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.getInterpreters(resource)).once(); + verify(interpreterService.triggerRefresh(anything())).never(); + }); + + test('Telemetry event is sent with useCachedInterpreter set to false if auto-selection has not been run before', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + + when(interpreterService.getInterpreters(resource)).thenCall(() => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.getInterpreters(resource)).once(); + sinon.assert.calledOnce(sendTelemetryEventStub); + expect(telemetryEvents).to.deep.equal( + [ + { + eventName: EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + properties: { useCachedInterpreter: false }, + }, + ], + 'Telemetry event properties are different', + ); + }); + + test('Telemetry event is sent with useCachedInterpreter set to true if auto-selection has been run before', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + + when(interpreterService.getInterpreters(resource)).thenCall(() => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.getInterpreters(resource)).once(); + sinon.assert.calledTwice(sendTelemetryEventStub); + expect(telemetryEvents).to.deep.equal( + [ + { + eventName: EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + properties: { useCachedInterpreter: false }, + }, + { + eventName: EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + properties: { useCachedInterpreter: true }, + }, + ], + 'Telemetry event properties are different', + ); + }); }); - 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 () => { + const queryState = mock(PersistentState) as PersistentState<boolean | undefined>; + + when(queryState.value).thenReturn(undefined); + when(stateFactory.createWorkspacePersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + when(queryState.value).thenReturn(undefined); + when(stateFactory.createGlobalPersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + let initialize = false; let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); - autoSelectionService.initializeStore = async () => (initialize = true as any); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = async () => { + initialize = true; + }; 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); await autoSelectionService.initializeStore(undefined); await autoSelectionService.initializeStore(undefined); await autoSelectionService.initializeStore(undefined); - verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).twice(); }); + 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + 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(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).twice(); 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), + ).thenReturn(instance(state)); + 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(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).twice(); 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); @@ -223,14 +510,7 @@ suite('Interpreters - Auto Selection', () => { 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'; @@ -238,12 +518,14 @@ suite('Interpreters - Auto Selection', () => { const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); when( - stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); when(state.value).thenReturn(interpreterInfoInState); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); @@ -255,6 +537,7 @@ suite('Interpreters - Auto Selection', () => { 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'; @@ -262,12 +545,14 @@ suite('Interpreters - Auto Selection', () => { const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); when( - stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); when(state.value).thenReturn(interpreterInfoInState); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); @@ -279,14 +564,15 @@ suite('Interpreters - Auto Selection', () => { 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); @@ -297,19 +583,22 @@ suite('Interpreters - Auto Selection', () => { 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); await autoSelectionService.initializeStore(undefined); @@ -320,15 +609,16 @@ suite('Interpreters - Auto Selection', () => { 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn(''); @@ -343,15 +633,16 @@ suite('Interpreters - Auto Selection', () => { 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); @@ -363,15 +654,16 @@ suite('Interpreters - Auto Selection', () => { 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<PythonInterpreter | undefined>( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( preferredGlobalInterpreter, - undefined - ) + undefined, + ), ).thenReturn(instance(state)); const globalInterpreterInfo = { path: 'global Value' }; when(state.value).thenReturn(globalInterpreterInfo as any); @@ -385,7 +677,7 @@ suite('Interpreters - Auto Selection', () => { const anotherResourceOfAnotherWorkspace = Uri.parse('Some other workspace'); when(workspaceService.getWorkspaceFolderIdentifier(anotherResourceOfAnotherWorkspace, anything())).thenReturn( - '2' + '2', ); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(anotherResourceOfAnotherWorkspace); diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts deleted file mode 100644 index 9db6976bdf3b..000000000000 --- a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -// 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 deleted file mode 100644 index aecb047c260e..000000000000 --- a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -// 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 deleted file mode 100644 index 7fd4a4779308..000000000000 --- a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -// 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 index b3e4409a2d72..ff82725da57e 100644 --- a/src/test/interpreters/autoSelection/proxy.unit.test.ts +++ b/src/test/interpreters/autoSelection/proxy.unit.test.ts @@ -3,43 +3,47 @@ '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 { PythonInterpreter } from '../../../client/pythonEnvironments/info'; +import { InterpreterAutoSelectionProxyService } from '../../../client/interpreter/autoSelection/proxy'; +import { IInterpreterAutoSelectionProxyService } from '../../../client/interpreter/autoSelection/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Interpreters - Auto Selection Proxy', () => { - class InstanceClass implements IInterpreterAutoSeletionProxyService { + class InstanceClass implements IInterpreterAutoSelectionProxyService { public eventEmitter = new EventEmitter<void>(); + constructor(private readonly pythonPath: string = '') {} + public get onDidChangeAutoSelectedInterpreter(): Event<void> { return this.eventEmitter.event; } - public getAutoSelectedInterpreter(_resource: Uri): PythonInterpreter { + + public getAutoSelectedInterpreter(): PythonEnvironment { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return { path: this.pythonPath } as any; } - public async setWorkspaceInterpreter( - _resource: Uri, - _interpreter: PythonInterpreter | undefined - ): Promise<void> { - return; + + // eslint-disable-next-line class-methods-use-this + public async setWorkspaceInterpreter(): Promise<void> { + return Promise.resolve(); } } - let proxy: InterpreterAutoSeletionProxyService; + let proxy: InterpreterAutoSelectionProxyService; setup(() => { - proxy = new InterpreterAutoSeletionProxyService([] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + proxy = new InterpreterAutoSelectionProxyService([] as any); }); - test('Change evnet is fired', () => { + test('Change event is fired', () => { const obj = new InstanceClass(); proxy.registerInstance(obj); let eventRaised = false; - proxy.onDidChangeAutoSelectedInterpreter(() => (eventRaised = true)); + proxy.onDidChangeAutoSelectedInterpreter(() => { + eventRaised = true; + }); proxy.registerInstance(obj); obj.eventEmitter.fire(); diff --git a/src/test/interpreters/autoSelection/rules/base.unit.test.ts b/src/test/interpreters/autoSelection/rules/base.unit.test.ts deleted file mode 100644 index 07f139739f85..000000000000 --- a/src/test/interpreters/autoSelection/rules/base.unit.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -// 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 { PythonInterpreter } from '../../../../client/pythonEnvironments/info'; - -suite('Interpreters - Auto Selection - Base Rule', () => { - let rule: BaseRuleServiceTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState<PythonInterpreter | 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: PythonInterpreter | undefined) { - return super.cacheSelectedInterpreter(resource, interpreter); - } - public async setGlobalInterpreter( - interpreter?: PythonInterpreter, - 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<PythonInterpreter | 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 deleted file mode 100644 index 790cd1e9e58c..000000000000 --- a/src/test/interpreters/autoSelection/rules/cached.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// 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 { PythonInterpreter } from '../../../../client/pythonEnvironments/info'; - -suite('Interpreters - Auto Selection - Cached Rule', () => { - let rule: CachedInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState<PythonInterpreter | 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?: PythonInterpreter, - 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<PythonInterpreter | 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 deleted file mode 100644 index a96448f6efe3..000000000000 --- a/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -// 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 { PythonInterpreter } from '../../../../client/pythonEnvironments/info'; - -suite('Interpreters - Auto Selection - Current Path Rule', () => { - let rule: CurrentPathInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState<PythonInterpreter | undefined>; - let locator: IInterpreterLocatorService; - let helper: IInterpreterHelper; - class CurrentPathInterpretersAutoSelectionRuleTest extends CurrentPathInterpretersAutoSelectionRule { - public async setGlobalInterpreter( - interpreter?: PythonInterpreter, - 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<PythonInterpreter | 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 deleted file mode 100644 index 805f4b6260bb..000000000000 --- a/src/test/interpreters/autoSelection/rules/settings.unit.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -// 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 { PythonInterpreter } from '../../../../client/pythonEnvironments/info'; - -suite('Interpreters - Auto Selection - Settings Rule', () => { - let rule: SettingsInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState<PythonInterpreter | 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<PythonInterpreter | 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 deleted file mode 100644 index b52bc51e8a6f..000000000000 --- a/src/test/interpreters/autoSelection/rules/system.unit.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -// 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 { PythonInterpreter } from '../../../../client/pythonEnvironments/info'; - -suite('Interpreters - Auto Selection - System Interpreters Rule', () => { - let rule: SystemWideInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState<PythonInterpreter | undefined>; - let interpreterService: IInterpreterService; - let helper: IInterpreterHelper; - class SystemWideInterpretersAutoSelectionRuleTest extends SystemWideInterpretersAutoSelectionRule { - public async setGlobalInterpreter( - interpreter?: PythonInterpreter, - 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<PythonInterpreter | 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 deleted file mode 100644 index ce7b7a303511..000000000000 --- a/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -// 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 { PythonInterpreter } from '../../../../client/pythonEnvironments/info'; - -suite('Interpreters - Auto Selection - Windows Registry Rule', () => { - let rule: WindowsRegistryInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState<PythonInterpreter | undefined>; - let locator: IInterpreterLocatorService; - let platform: IPlatformService; - let helper: IInterpreterHelper; - class WindowsRegistryInterpretersAutoSelectionRuleTest extends WindowsRegistryInterpretersAutoSelectionRule { - public async setGlobalInterpreter( - interpreter?: PythonInterpreter, - 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<PythonInterpreter | 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 deleted file mode 100644 index 8f0b7ebe9602..000000000000 --- a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -// 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 { PythonInterpreter } 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<PythonInterpreter | 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?: PythonInterpreter, - 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: PythonInterpreter | undefined) { - return super.cacheSelectedInterpreter(resource, interpreter); - } - public async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise<PythonInterpreter[] | 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<PythonInterpreter | 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 deleted file mode 100644 index 694aa1de63d9..000000000000 --- a/src/test/interpreters/currentPathService.unit.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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<PythonInterpreter[]>>; - 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<PythonInterpreter[]>>(); - 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', type: InterpreterType.Unknown }); - expect(interpreters).to.deep.include({ version, path: 'c:/python3', type: InterpreterType.Unknown }); - }); - }); -}); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index fd15ba158693..d9be806ff709 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -1,55 +1,49 @@ import { expect } from 'chai'; import * as path from 'path'; import { SemVer } from 'semver'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Disposable, EventEmitter, + LanguageStatusItem, + LanguageStatusSeverity, StatusBarAlignment, StatusBarItem, Uri, - WorkspaceFolder + WorkspaceFolder, } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { Commands, PYTHON_LANGUAGE } 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 { IDisposableRegistry, IPathUtils, ReadWrite } from '../../client/common/types'; +import { InterpreterQuickPickList } 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 + 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 { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info'; - -// tslint:disable:no-any max-func-body-length +import * as logging from '../../client/logging'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { ThemeColor } from '../mocks/vsc'; +import * as extapi from '../../client/envExt/api.internal'; -const info: PythonInterpreter = { +const info: PythonEnvironment = { architecture: Architecture.Unknown, companyDisplayName: '', - displayName: '', + detailedDisplayName: '', envName: '', path: '', - type: InterpreterType.Unknown, + envType: EnvironmentType.Unknown, version: new SemVer('0.0.0-alpha'), sysPrefix: '', - sysVersion: '' + sysVersion: '', }; suite('Interpreters Display', () => { @@ -57,36 +51,41 @@ suite('Interpreters Display', () => { 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 interpreterDisplay: IInterpreterDisplay & IExtensionSingleActivationService; let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; let pathUtils: TypeMoq.IMock<IPathUtils>; - let output: TypeMoq.IMock<IOutputChannel>; - let autoSelection: IInterpreterAutoSelectionService; - setup(() => { + let languageStatusItem: TypeMoq.IMock<LanguageStatusItem>; + let traceLogStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; + async function createInterpreterDisplay(filters: IInterpreterStatusbarVisibilityFilter[] = []) { + interpreterDisplay = new InterpreterDisplay(serviceContainer.object); + try { + await interpreterDisplay.activate(); + } catch {} + filters.forEach((f) => interpreterDisplay.registerVisibilityFilter(f)); + } + + async function setupMocks(useLanguageStatus: boolean) { + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + 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>(); + statusBar.setup((s) => s.name).returns(() => ''); + languageStatusItem = TypeMoq.Mock.ofType<LanguageStatusItem>(); 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); + traceLogStub = sinon.stub(logging, 'traceLog'); + serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); @@ -96,31 +95,33 @@ suite('Interpreters Display', () => { 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); + if (!useLanguageStatus) { + applicationShell + .setup((a) => + a.createStatusBarItem( + TypeMoq.It.isValue(StatusBarAlignment.Right), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => statusBar.object); + } else { + applicationShell + .setup((a) => + a.createLanguageStatusItem(TypeMoq.It.isAny(), TypeMoq.It.isValue({ language: PYTHON_LANGUAGE })), + ) + .returns(() => languageStatusItem.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); + await createInterpreterDisplay(); } + function setupWorkspaceFolder(resource: Uri, workspaceFolder?: Uri) { if (workspaceFolder) { const mockFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); @@ -132,238 +133,321 @@ suite('Interpreters Display', () => { 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: PythonInterpreter = { - ...info, - displayName: 'Dummy_Display_Name', - type: InterpreterType.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: PythonInterpreter = { - ...info, - displayName: 'Dummy_Display_Name', - type: InterpreterType.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: PythonInterpreter = ({ - displayName, - path: pythonPath - } as any) as PythonInterpreter; - 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: PythonInterpreter = { - ...info, - displayName: 'Dummy_Display_Name', - type: InterpreterType.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: PythonInterpreter = { - ...info, - displayName: 'Dummy_Display_Name', - type: InterpreterType.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); + [false].forEach((useLanguageStatus) => { + suite(`When ${useLanguageStatus ? `using language status` : 'using status bar'}`, () => { + setup(async () => { + setupMocks(useLanguageStatus); + }); - 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 = { visible: false }; - const filter2: IInterpreterStatusbarVisibilityFilter = { visible: true }; - createInterpreterDisplay([filter1, filter2]); + teardown(() => { + sinon.restore(); + }); + test('Statusbar must be created and have command name initialized', () => { + if (useLanguageStatus) { + languageStatusItem.verify( + (s) => (s.severity = TypeMoq.It.isValue(LanguageStatusSeverity.Information)), + TypeMoq.Times.once(), + ); + languageStatusItem.verify( + (s) => + (s.command = TypeMoq.It.isValue({ + title: InterpreterQuickPickList.browsePath.openButtonLabel, + command: Commands.Set_Interpreter, + })), + TypeMoq.Times.once(), + ); + expect(disposableRegistry).contain(languageStatusItem.object); + } else { + statusBar.verify((s) => (s.command = TypeMoq.It.isAny()), TypeMoq.Times.once()); + expect(disposableRegistry).contain(statusBar.object); + } + expect(disposableRegistry).to.be.lengthOf.above(0); + }); + 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, + detailedDisplayName: 'Dummy_Display_Name', + envType: EnvironmentType.Unknown, + path: path.join('user', 'development', 'env', 'bin', 'python'), + }; + setupWorkspaceFolder(resource, workspaceFolder); + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => []); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(activeInterpreter)); - await interpreterDisplay.refresh(resource); + 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 = { visible: false }; - const filter2: IInterpreterStatusbarVisibilityFilter = { visible: false }; - createInterpreterDisplay([filter1, filter2]); + if (useLanguageStatus) { + languageStatusItem.verify( + (s) => (s.text = TypeMoq.It.isValue(activeInterpreter.detailedDisplayName)!), + TypeMoq.Times.once(), + ); + languageStatusItem.verify( + (s) => (s.detail = TypeMoq.It.isValue(activeInterpreter.path)!), + TypeMoq.Times.atLeastOnce(), + ); + } else { + statusBar.verify( + (s) => (s.text = TypeMoq.It.isValue(activeInterpreter.detailedDisplayName)!), + 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, + detailedDisplayName: '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); + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => []); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(activeInterpreter)); - await interpreterDisplay.refresh(resource); + await interpreterDisplay.refresh(resource); + traceLogStub.calledOnceWithExactly( + `Python interpreter path: ${activeInterpreter.path}`, + activeInterpreter.path, + ); + }); + 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 = 'Python 3.10.1'; + const expectedDisplayName = '3.10.1'; - 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 = { visible: true }; - const filter2: IInterpreterStatusbarVisibilityFilter = { visible: true }; - createInterpreterDisplay([filter1, filter2]); + setupWorkspaceFolder(resource, workspaceFolder); + const pythonInterpreter: PythonEnvironment = ({ + detailedDisplayName: displayName, + path: pythonPath, + } as any) as PythonEnvironment; + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(pythonInterpreter)); - await interpreterDisplay.refresh(resource); + await interpreterDisplay.refresh(resource); + if (useLanguageStatus) { + languageStatusItem.verify( + (s) => (s.detail = TypeMoq.It.isValue(pythonPath)), + TypeMoq.Times.atLeastOnce(), + ); + languageStatusItem.verify( + (s) => (s.text = TypeMoq.It.isValue(expectedDisplayName)), + TypeMoq.Times.once(), + ); + } else { + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)), TypeMoq.Times.atLeastOnce()); + statusBar.verify((s) => (s.text = TypeMoq.It.isValue(expectedDisplayName)), 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); - 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> = { visible: true, changed: event1.event }; - const event2 = new EventEmitter<void>(); - const filter2: ReadWrite<IInterpreterStatusbarVisibilityFilter> = { visible: true, 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.visible = false; - 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.visible = true; - statusBar.reset(); - event2.fire(); - - // No changes. - statusBar.verify((s) => s.show(), TypeMoq.Times.once()); - statusBar.verify((s) => s.hide(), TypeMoq.Times.never()); + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => [{} as any]); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(undefined)); + 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)); + + await interpreterDisplay.refresh(resource); + + if (useLanguageStatus) { + languageStatusItem.verify( + (s) => (s.text = TypeMoq.It.isValue('$(alert) No Interpreter Selected')), + TypeMoq.Times.once(), + ); + } else { + statusBar.verify( + (s) => + (s.backgroundColor = TypeMoq.It.isValue(new ThemeColor('statusBarItem.warningBackground'))), + TypeMoq.Times.once(), + ); + statusBar.verify((s) => (s.color = TypeMoq.It.isValue('')), TypeMoq.Times.once()); + statusBar.verify( + (s) => + (s.text = TypeMoq.It.isValue( + `$(alert) ${InterpreterQuickPickList.browsePath.openButtonLabel}`, + )), + 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, + detailedDisplayName: 'Dummy_Display_Name', + envType: EnvironmentType.Unknown, + companyDisplayName: 'Company Name', + path: pythonPath, + }; + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + 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(); + if (useLanguageStatus) { + languageStatusItem.verify( + (s) => (s.text = TypeMoq.It.isValue(activeInterpreter.detailedDisplayName)!), + TypeMoq.Times.once(), + ); + languageStatusItem.verify( + (s) => (s.detail = TypeMoq.It.isValue(pythonPath)!), + TypeMoq.Times.atLeastOnce(), + ); + } else { + statusBar.verify( + (s) => (s.text = TypeMoq.It.isValue(activeInterpreter.detailedDisplayName)!), + TypeMoq.Times.once(), + ); + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)!), TypeMoq.Times.atLeastOnce()); + } + }); + suite('Visibility', () => { + const resource = Uri.file('x'); + suiteSetup(function () { + if (useLanguageStatus) { + return this.skip(); + } + }); + setup(() => { + const workspaceFolder = Uri.file('workspace'); + const activeInterpreter: PythonEnvironment = { + ...info, + detailedDisplayName: 'Dummy_Display_Name', + envType: EnvironmentType.Unknown, + path: path.join('user', 'development', 'env', 'bin', 'python'), + }; + setupWorkspaceFolder(resource, workspaceFolder); + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => []); + 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 deleted file mode 100644 index f411e12f7220..000000000000 --- a/src/test/interpreters/display/interpreterSelectionTip.unit.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// 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 { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, Interpreters } from '../../../client/common/utils/localize'; -import { InterpreterSelectionTip } from '../../../client/interpreter/display/interpreterSelectionTip'; - -// tslint:disable:no-any -suite('Interpreters - Interpreter Selection Tip', () => { - let selectionTip: InterpreterSelectionTip; - let appShell: IApplicationShell; - let storage: IPersistentState<boolean>; - setup(() => { - const factory = mock(PersistentStateFactory); - storage = mock(PersistentState); - appShell = mock(ApplicationShell); - - when(factory.createGlobalPersistentState('InterpreterSelectionTip', false)).thenReturn(instance(storage)); - - selectionTip = new InterpreterSelectionTip(instance(appShell), instance(factory)); - }); - test('Do not show tip', async () => { - when(storage.value).thenReturn(true); - - await selectionTip.activate(); - - verify(appShell.showInformationMessage(anything(), anything())).never(); - }); - test('Show tip and do not track it', async () => { - when(storage.value).thenReturn(false); - when(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).thenResolve(); - - await selectionTip.activate(); - - verify(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).once(); - verify(storage.updateValue(true)).never(); - }); - test('Show tip and track it', async () => { - when(storage.value).thenReturn(false); - when(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).thenResolve( - Common.gotIt() as any - ); - - await selectionTip.activate(); - - verify(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).once(); - verify(storage.updateValue(true)).once(); - }); -}); diff --git a/src/test/interpreters/display/progressDisplay.unit.test.ts b/src/test/interpreters/display/progressDisplay.unit.test.ts index 8dee81840f79..b1acecd44434 100644 --- a/src/test/interpreters/display/progressDisplay.unit.test.ts +++ b/src/test/interpreters/display/progressDisplay.unit.test.ts @@ -3,83 +3,86 @@ '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'; +import { Commands } from '../../../client/common/constants'; +import { createDeferred, Deferred } from '../../../client/common/utils/async'; +import { Interpreters } from '../../../client/common/utils/localize'; +import { IComponentAdapter } from '../../../client/interpreter/contracts'; +import { InterpreterLocatorProgressStatusBarHandler } from '../../../client/interpreter/display/progressDisplay'; +import { ProgressNotificationEvent, ProgressReportStage } from '../../../client/pythonEnvironments/base/locator'; +import { noop } from '../../core'; type ProgressTask<R> = ( progress: Progress<{ message?: string; increment?: number }>, - token: CancellationToken + 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 () => { + let refreshingCallback: (e: ProgressNotificationEvent) => unknown | undefined; + let refreshDeferred: Deferred<void>; + let componentAdapter: IComponentAdapter; + setup(() => { + refreshDeferred = createDeferred<void>(); + componentAdapter = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onProgress(listener: (e: ProgressNotificationEvent) => any): Disposable { + refreshingCallback = listener; + return { dispose: noop }; + }, + getRefreshPromise: () => refreshDeferred.promise, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + teardown(() => { + refreshDeferred.resolve(); + }); + test('Display discovering message when refreshing interpreters for the first time', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); - statusBar.register(); - refreshingCallback(undefined); + await statusBar.activate(); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - const options = capture(shell.withProgress as any).last()[0] as ProgressOptions; - expect(options.title).to.be.equal(Common.loadingExtension()); + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(`[${Interpreters.discovering}](command:${Commands.Set_Interpreter})`); }); test('Display refreshing message when refreshing interpreters for the second time', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); - statusBar.register(); - refreshingCallback(undefined); + await statusBar.activate(); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - let options = capture(shell.withProgress as any).last()[0] as ProgressOptions; - expect(options.title).to.be.equal(Common.loadingExtension()); + let options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(`[${Interpreters.discovering}](command:${Commands.Set_Interpreter})`); - refreshingCallback(undefined); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - options = capture(shell.withProgress as any).last()[0] as ProgressOptions; - expect(options.title).to.be.equal(Interpreters.refreshing()); + options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(`[${Interpreters.refreshing}](command:${Commands.Set_Interpreter})`); }); test('Progress message is hidden when loading has completed', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); - statusBar.register(); - refreshingCallback(undefined); + await statusBar.activate(); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - 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); + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + const callback = capture(shell.withProgress as never).last()[1] as ProgressTask<void>; + const promise = callback(undefined as never, undefined as never); - expect(options.title).to.be.equal(Common.loadingExtension()); + expect(options.title).to.be.equal(`[${Interpreters.discovering}](command:${Commands.Set_Interpreter})`); - refreshedCallback(undefined); + refreshDeferred.resolve(); // 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 index 481e6bb5ed31..3f64d5a26580 100644 --- a/src/test/interpreters/helpers.unit.test.ts +++ b/src/test/interpreters/helpers.unit.test.ts @@ -8,22 +8,21 @@ 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 { IComponentAdapter } from '../../client/interpreter/contracts'; 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>; + let pyenvs: TypeMoq.IMock<IComponentAdapter>; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - hashProviderFactory = TypeMoq.Mock.ofType<IInterpreterHashProviderFactory>(); + pyenvs = TypeMoq.Mock.ofType<IComponentAdapter>(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) @@ -32,7 +31,7 @@ suite('Interpreters Display Helper', () => { .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) .returns(() => documentManager.object); - helper = new InterpreterHelper(serviceContainer.object, hashProviderFactory.object); + helper = new InterpreterHelper(serviceContainer.object, pyenvs.object); }); test('getActiveWorkspaceUri should return undefined if there are no workspaces', () => { workspaceService.setup((w) => w.workspaceFolders).returns(() => []); @@ -42,7 +41,7 @@ suite('Interpreters Display Helper', () => { }); 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); @@ -52,7 +51,7 @@ suite('Interpreters Display Helper', () => { }); 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); @@ -62,7 +61,7 @@ suite('Interpreters Display Helper', () => { 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>(); @@ -78,14 +77,14 @@ suite('Interpreters Display Helper', () => { 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(() => { diff --git a/src/test/interpreters/interpreterPathCommand.unit.test.ts b/src/test/interpreters/interpreterPathCommand.unit.test.ts new file mode 100644 index 000000000000..8d45ad82577c --- /dev/null +++ b/src/test/interpreters/interpreterPathCommand.unit.test.ts @@ -0,0 +1,95 @@ +// 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, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IDisposable } from '../../client/common/types'; +import * as commandApis from '../../client/common/vscodeApis/commandApis'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Interpreter Path Command', () => { + let interpreterService: IInterpreterService; + let interpreterPathCommand: InterpreterPathCommand; + let registerCommandStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + + setup(() => { + interpreterService = mock<IInterpreterService>(); + registerCommandStub = sinon.stub(commandApis, 'registerCommand'); + interpreterPathCommand = new InterpreterPathCommand(instance(interpreterService), []); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure command is registered with the correct callback handler', async () => { + let getInterpreterPathHandler = (_param: unknown) => undefined; + registerCommandStub.callsFake((_, cb) => { + getInterpreterPathHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + await interpreterPathCommand.activate(); + + sinon.assert.calledOnce(registerCommandStub); + 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', type: 'debugpy' }; + when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.file('folderPath')); + + return Promise.resolve({ path: 'settingValue' }) as unknown; + }); + const setting = await 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(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.file('folderPath')); + + return Promise.resolve({ path: 'settingValue' }) as unknown; + }); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); + + test('If interpreter path contains spaces, double quote it before returning', async () => { + const args = ['command', 'folderPath']; + when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.file('folderPath')); + + return Promise.resolve({ path: 'setting Value' }) as unknown; + }); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('"setting Value"'); + }); + + test('If neither of these exists, value of workspace folder is `undefined`', async () => { + getConfigurationStub.withArgs('python').returns({ + get: sinon.stub().returns(false), + }); + + const args = ['command']; + + when(interpreterService.getActiveInterpreter(undefined)).thenReturn( + Promise.resolve({ path: 'settingValue' }) as Promise<PythonEnvironment | undefined>, + ); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); +}); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index 8bcb47a423a9..1d521dad8ec8 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -3,98 +3,93 @@ '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 * as sinon from 'sinon'; 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 { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; import { IPythonExecutionFactory, IPythonExecutionService } from '../../client/common/process/types'; import { IConfigurationService, IDisposableRegistry, - IExperimentsManager, + IExperimentService, + IInstaller, IInterpreterPathService, InterpreterConfigurationScope, - IPersistentState, IPersistentStateFactory, - IPythonSettings + 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 + IInterpreterAutoSelectionProxyService, } from '../../client/interpreter/autoSelection/types'; import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; -import { - IInterpreterDisplay, - IInterpreterHelper, - IInterpreterLocatorService, - INTERPRETER_LOCATOR_SERVICE -} from '../../client/interpreter/contracts'; +import { IComponentAdapter, IInterpreterDisplay, IInterpreterHelper } 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 { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info'; import { PYTHON_PATH } from '../common'; import { MockAutoSelectionService } from '../mocks/autoSelector'; +import * as proposedApi from '../../client/environmentApi'; +import { createTypeMoq } from '../mocks/helper'; +import * as extapi from '../../client/envExt/api.internal'; -use(chaiAsPromised); +/* eslint-disable @typescript-eslint/no-explicit-any */ + +use(chaiAsPromised.default); suite('Interpreters service', () => { let serviceManager: ServiceManager; let serviceContainer: ServiceContainer; let updater: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + let pyenvs: TypeMoq.IMock<IComponentAdapter>; 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>; + let experiments: TypeMoq.IMock<IExperimentService>; + let installer: TypeMoq.IMock<IInstaller>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let reportActiveInterpreterChangedStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); - 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>(); + interpreterPathService = createTypeMoq<IInterpreterPathService>(); + updater = createTypeMoq<IPythonPathUpdaterServiceManager>(); + pyenvs = createTypeMoq<IComponentAdapter>(); + helper = createTypeMoq<IInterpreterHelper>(); + workspace = createTypeMoq<IWorkspaceService>(); + config = createTypeMoq<WorkspaceConfiguration>(); + fileSystem = createTypeMoq<IFileSystem>(); + interpreterDisplay = createTypeMoq<IInterpreterDisplay>(); + persistentStateFactory = createTypeMoq<IPersistentStateFactory>(); + pythonExecutionFactory = createTypeMoq<IPythonExecutionFactory>(); + pythonExecutionService = createTypeMoq<IPythonExecutionService>(); + configService = createTypeMoq<IConfigurationService>(); + installer = createTypeMoq<IInstaller>(); + appShell = createTypeMoq<IApplicationShell>(); + experiments = createTypeMoq<IExperimentService>(); + + pythonSettings = createTypeMoq<IPythonSettings>(); pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); @@ -108,558 +103,206 @@ suite('Interpreters service', () => { .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => { const state = { - updateValue: () => Promise.resolve() + updateValue: () => Promise.resolve(), }; return state as any; }); + serviceManager.addSingletonInstance<IExperimentService>(IExperimentService, experiments.object); serviceManager.addSingletonInstance<Disposable[]>(IDisposableRegistry, []); serviceManager.addSingletonInstance<IInterpreterHelper>(IInterpreterHelper, helper.object); serviceManager.addSingletonInstance<IPythonPathUpdaterServiceManager>( IPythonPathUpdaterServiceManager, - updater.object + 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 + interpreterPathService.object, ); serviceManager.addSingletonInstance<IInterpreterDisplay>(IInterpreterDisplay, interpreterDisplay.object); - serviceManager.addSingletonInstance<IVirtualEnvironmentManager>( - IVirtualEnvironmentManager, - virtualEnvMgr.object - ); serviceManager.addSingletonInstance<IPersistentStateFactory>( IPersistentStateFactory, - persistentStateFactory.object + persistentStateFactory.object, ); serviceManager.addSingletonInstance<IPythonExecutionFactory>( IPythonExecutionFactory, - pythonExecutionFactory.object + pythonExecutionFactory.object, ); serviceManager.addSingletonInstance<IPythonExecutionService>( IPythonExecutionService, - pythonExecutionService.object + pythonExecutionService.object, ); serviceManager.addSingleton<IInterpreterAutoSelectionService>( IInterpreterAutoSelectionService, - MockAutoSelectionService + MockAutoSelectionService, ); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( - IInterpreterAutoSeletionProxyService, - MockAutoSelectionService + serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, ); + installer.setup((i) => i.isInstalled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + serviceManager.addSingletonInstance<IInstaller>(IInstaller, installer.object); + serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); 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()); - }); + reportActiveInterpreterChangedStub = sinon.stub(proposedApi, 'reportActiveInterpreterChanged'); + }); - 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()); - }); + teardown(() => { + sinon.restore(); + }); - 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(); - }); + [undefined, Uri.file('xyz')].forEach((resource) => { + const resourceTestSuffix = `(${resource ? 'with' : 'without'} a resource)`; - 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)); + test(`Refresh invokes refresh of display ${resourceTestSuffix}`, async () => { interpreterDisplay - .setup((i) => i.refresh()) - .returns(() => Promise.resolve()) + .setup((i) => i.refresh(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(undefined)) .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(); - }); + const service = new InterpreterService(serviceContainer, pyenvs.object); + await service.refresh(resource); - 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(InterpreterType.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'); + test('Changes to active document should invoke interpreter.refresh method', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq<IDocumentManager>(); + + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let activeTextEditorChangeHandler: (e: TextEditor | undefined) => any | undefined; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; }); - }); - }); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - suite('Caching Display name', () => { - setup(() => { - setupSuite(); - fileSystem.reset(); - persistentStateFactory.reset(); - }); - test('Return cached display name', async () => { - const pythonPath = '1234'; - const interpreterInfo: Partial<PythonInterpreter> = { 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()); + service.initialize(); + const textEditor = createTypeMoq<TextEditor>(); + const uri = Uri.file(path.join('usr', 'file.py')); + const document = createTypeMoq<TextDocument>(); + textEditor.setup((t) => t.document).returns(() => document.object); + document.setup((d) => d.uri).returns(() => uri); + activeTextEditorChangeHandler!(textEditor.object); - const service = new InterpreterService(serviceContainer, hashProviderFactory.object); - const displayName = await service.getDisplayName(interpreterInfo, undefined); + interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(uri)), TypeMoq.Times.once()); + }); - 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<PythonInterpreter> = { 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()); + test('If there is no active document then interpreter.refresh should not be invoked', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq<IDocumentManager>(); + + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let activeTextEditorChangeHandler: (e?: TextEditor | undefined) => any | undefined; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - const service = new InterpreterService(serviceContainer, hashProviderFactory.object); - const displayName = await service.getDisplayName(interpreterInfo, undefined).catch(() => ''); + service.initialize(); + activeTextEditorChangeHandler!(); - expect(displayName).to.not.equal(expectedDisplayName); - hashProviderFactory.verifyAll(); - hashProvider.verifyAll(); - persistentStateFactory.verifyAll(); - }); + interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); }); - // 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<InterpreterType>(InterpreterType) as ( - | { name: string; value: InterpreterType } - | 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<PythonInterpreter> = { - version, - architecture: arch ? arch.value : undefined, - envName, - type: interpreterType ? interpreterType.value : undefined, - path: pythonPath - }; - - if ( - interpreterInfo.path && - interpreterType && - interpreterType.value === InterpreterType.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<PythonInterpreter>) { - 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.type && - interpreterInfo.type === InterpreterType.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.type) { - envSuffixParts.push(`${interpreterType!.name}_display`); - } - - const envSuffix = - envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; - return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); - } - }); - }); - }); - }); - }); - }); - }); + test('Register the correct handler', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq<IDocumentManager>(); + + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let interpreterPathServiceHandler: (e: InterpreterConfigurationScope) => any | undefined = () => 0; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => ({ dispose: noop })); + const i: InterpreterConfigurationScope = { + uri: Uri.parse('a'), + configTarget: ConfigurationTarget.Workspace, + }; + configService.reset(); + 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(() => ({ dispose: noop })); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + interpreterDisplay.setup((a) => a.refresh()).returns(() => Promise.resolve()); + + service.initialize(); + expect(interpreterPathServiceHandler).to.not.equal(undefined, 'Handler not set'); + + await interpreterPathServiceHandler!(i); + + // Ensure correct handler was invoked + configService.verifyAll(); }); - suite('Interpreter Cache', () => { - setup(() => { - setupSuite(); - fileSystem.reset(); - persistentStateFactory.reset(); + test('If stored setting is an empty string, refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const resource = Uri.parse('a'); + const workspaceFolder = { uri: resource, name: '', index: 0 }; + workspace.setup((w) => w.getWorkspaceFolder(resource)).returns(() => workspaceFolder); + 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()); + await service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { + path: 'current path', + resource: workspaceFolder, }); - 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?: PythonInterpreter }>>(); - const info = { path: 'hell', type: InterpreterType.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('If stored setting is not equal to current interpreter path setting, refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const resource = Uri.parse('a'); + const workspaceFolder = { uri: resource, name: '', index: 0 }; + workspace.setup((w) => w.getWorkspaceFolder(resource)).returns(() => workspaceFolder); + 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()); + await service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { + path: 'current path', + resource: workspaceFolder, }); - 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?: PythonInterpreter }>>(); - const info = { path: 'hell', type: InterpreterType.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(); - }); + test('If stored setting is equal to current interpreter path setting, do not refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.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()); + await service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + expect(reportActiveInterpreterChangedStub.notCalled).to.be.equal(true); }); }); diff --git a/src/test/interpreters/interpreterVersion.unit.test.ts b/src/test/interpreters/interpreterVersion.unit.test.ts deleted file mode 100644 index 5c9acf952dc8..000000000000 --- a/src/test/interpreters/interpreterVersion.unit.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 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 index e32e5028c039..12401115eb36 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -2,13 +2,13 @@ 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'; +import { MockMemento } from '../mocks/mementos'; @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 }[] + 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) => { @@ -24,7 +24,7 @@ export class MockRegistry implements IRegistry { key: string, hive: RegistryHive, arch?: Architecture, - name?: string + name?: string, ): Promise<string | undefined | null> { const items = this.values.find((item) => { if (item.key !== key || item.hive !== hive) { @@ -43,34 +43,15 @@ export class MockRegistry implements IRegistry { } } -// 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 + + public readonly storage = new MockMemento(); + 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 index 5ba72dc2c1bf..5c851b8071f3 100644 --- a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -1,347 +1,134 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { ConfigurationTarget, Uri } from 'vscode'; import { IWorkspaceService } from '../../client/common/application/types'; -import { DeprecatePythonPath } from '../../client/common/experiments/groups'; -import { IExperimentsManager, IInterpreterPathService } from '../../client/common/types'; +import { IExperimentService, 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 experimentsManager: TypeMoq.IMock<IExperimentService>; let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; let updaterServiceFactory: IPythonPathUpdaterServiceFactory; - function setupMocks(inExperiment: boolean = false) { + function setupMocks() { 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>(); + experimentsManager = TypeMoq.Mock.ofType<IExperimentService>(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IExperimentsManager))) + .setup((c) => c.get(TypeMoq.It.isValue(IExperimentService))) .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('Global', () => { + setup(() => setupMocks()); + 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(); }); - 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() - ); - }); + 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('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()); + 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(); }); - - 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(); - }); + 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(); }); - 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'); + }); + suite('Workspace (multiroot scenario)', () => { + setup(() => setupMocks()); + 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, expectedPythonPath)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); + 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); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); - interpreterPathService.verifyAll(); - }); + interpreterPathService.verifyAll(); }); }); }); diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index 5e166318533a..ad8614b42d8b 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -3,67 +3,49 @@ '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 { InterpreterAutoSelectionProxyService } from '../../client/interpreter/autoSelection/proxy'; import { - AutoSelectionRule, - IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService, - IInterpreterEvaluation, - IInterpreterSecurityService, - IInterpreterSecurityStorage + IInterpreterAutoSelectionProxyService, } from '../../client/interpreter/autoSelection/types'; -import { InterpreterComparer } from '../../client/interpreter/configuration/interpreterComparer'; +import { EnvironmentTypeComparer } from '../../client/interpreter/configuration/environmentTypeComparer'; +import { InstallPythonCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython'; +import { InstallPythonViaTerminal } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; 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, + IInterpreterQuickPick, IInterpreterSelector, IPythonPathUpdaterServiceFactory, - IPythonPathUpdaterServiceManager + IPythonPathUpdaterServiceManager, + IRecommendedEnvironmentService, } from '../../client/interpreter/configuration/types'; import { + IActivatedEnvironmentLaunch, 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 { InterpreterLocatorProgressStatusBarHandler } from '../../client/interpreter/display/progressDisplay'; 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 { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; 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'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; suite('Interpreters - Service Registry', () => { test('Registrations', () => { @@ -71,56 +53,37 @@ suite('Interpreters - Service Registry', () => { registerTypes(instance(serviceManager)); [ + [IExtensionSingleActivationService, InstallPythonCommand], + [IExtensionSingleActivationService, InstallPythonViaTerminal], [IExtensionSingleActivationService, SetInterpreterCommand], + [IInterpreterQuickPick, 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], - + [IRecommendedEnvironmentService, RecommendedEnvironmentService], [IInterpreterSelector, InterpreterSelector], - [IShebangCodeLensProvider, ShebangCodeLensProvider], [IInterpreterHelper, InterpreterHelper], - [IInterpreterComparer, InterpreterComparer], + [IInterpreterComparer, EnvironmentTypeComparer], - [IInterpreterLocatorProgressHandler, InterpreterLocatorProgressStatubarHandler], + [IExtensionSingleActivationService, InterpreterLocatorProgressStatusBarHandler], - [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], + [IInterpreterAutoSelectionProxyService, InterpreterAutoSelectionProxyService], [IInterpreterAutoSelectionService, InterpreterAutoSelectionService], [EnvironmentActivationService, EnvironmentActivationService], - [TerminalEnvironmentActivationService, TerminalEnvironmentActivationService], [IEnvironmentActivationService, EnvironmentActivationService], - [IExtensionActivationService, CondaInheritEnvPrompt] + [IExtensionSingleActivationService, InterpreterPathCommand], + [IExtensionActivationService, CondaInheritEnvPrompt], + [IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch], ].forEach((mapping) => { - verify(serviceManager.addSingleton.apply(serviceManager, mapping as any)).once(); + // eslint-disable-next-line prefer-spread + verify(serviceManager.addSingleton.apply(serviceManager, mapping as never)).once(); }); }); }); diff --git a/src/test/interpreters/virtualEnvManager.unit.test.ts b/src/test/interpreters/virtualEnvManager.unit.test.ts deleted file mode 100644 index 64d672d349a5..000000000000 --- a/src/test/interpreters/virtualEnvManager.unit.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -// 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 { IPipEnvService } 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(IPipEnvService))).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(IPipEnvService))).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/activatedEnvLaunch.unit.test.ts b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts new file mode 100644 index 000000000000..860970bd641e --- /dev/null +++ b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts @@ -0,0 +1,528 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { ExecutionResult, IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { Common } from '../../../client/common/utils/localize'; +import { IPythonPathUpdaterServiceManager } from '../../../client/interpreter/configuration/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { ActivatedEnvironmentLaunch } from '../../../client/interpreter/virtualEnvs/activatedEnvLaunch'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { Conda } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; + +suite('Activated Env Launch', async () => { + const uri = Uri.file('a'); + const condaPrefix = 'path/to/conda/env'; + const virtualEnvPrefix = 'path/to/virtual/env'; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let pythonPathUpdaterService: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let processServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let processService: TypeMoq.IMock<IProcessService>; + let activatedEnvLaunch: ActivatedEnvironmentLaunch; + let _promptIfApplicable: sinon.SinonStub; + + suite('Method selectIfLaunchedViaActivatedEnv()', () => { + const oldVSCodeCLI = process.env.VSCODE_CLI; + const oldCondaPrefix = process.env.CONDA_PREFIX; + const oldCondaShlvl = process.env.CONDA_SHLVL; + const oldVirtualEnv = process.env.VIRTUAL_ENV; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + pythonPathUpdaterService = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + _promptIfApplicable = sinon.stub(ActivatedEnvironmentLaunch.prototype, '_promptIfApplicable'); + _promptIfApplicable.returns(Promise.resolve()); + process.env.VSCODE_CLI = '1'; + }); + + teardown(() => { + if (oldCondaPrefix) { + process.env.CONDA_PREFIX = oldCondaPrefix; + } else { + delete process.env.CONDA_PREFIX; + } + if (oldCondaShlvl) { + process.env.CONDA_SHLVL = oldCondaShlvl; + } else { + delete process.env.CONDA_SHLVL; + } + if (oldVirtualEnv) { + process.env.VIRTUAL_ENV = oldVirtualEnv; + } else { + delete process.env.VIRTUAL_ENV; + } + if (oldVSCodeCLI) { + process.env.VSCODE_CLI = oldVSCodeCLI; + } else { + delete process.env.VSCODE_CLI; + } + sinon.restore(); + }); + + test('Updates interpreter path with the non-base conda prefix if activated', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Does not update interpreter path if VSCode is not launched via CLI', async () => { + delete process.env.VSCODE_CLI; + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path with the base conda prefix if activated and environment var is configured to not auto activate it', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + process.env.CONDA_AUTO_ACTIVATE_BASE = 'false'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path with the base conda prefix if activated and environment var is configured to auto activate it', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + process.env.CONDA_AUTO_ACTIVATE_BASE = 'true'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + expect(_promptIfApplicable.calledOnce).to.equal(true, 'Prompt not displayed'); + }); + + test('Updates interpreter path with virtual env prefix if activated', async () => { + process.env.VIRTUAL_ENV = virtualEnvPrefix; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(virtualEnvPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(virtualEnvPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path in global scope if no workspace is opened', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('load'), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + expect(_promptIfApplicable.notCalled).to.equal(true, 'Prompt should not be displayed'); + }); + + test('Returns `undefined` if env was already selected', async () => { + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + true, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + }); + }); + + suite('Method _promptIfApplicable()', () => { + const oldCondaPrefix = process.env.CONDA_PREFIX; + const oldCondaShlvl = process.env.CONDA_SHLVL; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + pythonPathUpdaterService = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((p) => (p as any).then).returns(() => undefined); + sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); + }); + + teardown(() => { + if (oldCondaPrefix) { + process.env.CONDA_PREFIX = oldCondaPrefix; + } else { + delete process.env.CONDA_PREFIX; + } + if (oldCondaShlvl) { + process.env.CONDA_SHLVL = oldCondaShlvl; + } else { + delete process.env.CONDA_SHLVL; + } + sinon.restore(); + }); + + test('Shows prompt if base conda environment is activated and auto activate configuration is disabled', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('If user chooses yes, update interpreter path', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + pythonPathUpdaterService.verifyAll(); + }); + + test('If user chooses no, do not update interpreter path', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelNo)); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + pythonPathUpdaterService.verifyAll(); + }); + + test('Do not show prompt if base conda environment is activated but auto activate configuration is enabled', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base True' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('Do not show prompt if non-base conda environment is activated', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'nonbase' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('Do not show prompt if conda environment is not activated', async () => { + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + }); +}); diff --git a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts index f48ea816c044..9499b5294d78 100644 --- a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts @@ -8,29 +8,30 @@ 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 { + IApplicationEnvironment, + 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 { 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 + condaInheritEnvPromptKey, } from '../../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; -import { InterpreterType } from '../../../client/pythonEnvironments/info'; - -// tslint:disable:no-any +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; -// 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 applicationEnvironment: TypeMoq.IMock<IApplicationEnvironment>; let persistentStateFactory: IPersistentStateFactory; let notificationPromptEnabled: TypeMoq.IMock<IPersistentState<any>>; let condaInheritEnvPrompt: CondaInheritEnvPrompt; @@ -44,28 +45,29 @@ suite('Conda Inherit Env Prompt', async () => { 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>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object + platformService.object, + applicationEnvironment.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 + applicationEnvironment.object, + true, ); const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); interpreterService @@ -81,6 +83,14 @@ suite('Conda Inherit Env Prompt', async () => { expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); verifyAll(); }); + test('Returns false if running on remote', async () => { + applicationEnvironment.reset(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => 'ssh'); + 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 on Windows', async () => { platformService .setup((ps) => ps.isWindows) @@ -93,7 +103,7 @@ suite('Conda Inherit Env Prompt', async () => { }); test('Returns false if active interpreter is not of type Conda', async () => { const interpreter = { - type: InterpreterType.Pipenv + envType: EnvironmentType.Pipenv, }; const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); platformService @@ -134,7 +144,7 @@ suite('Conda Inherit Env Prompt', async () => { }); test('Returns false if settings returned is `undefined`', async () => { const interpreter = { - type: InterpreterType.Conda + envType: EnvironmentType.Conda, }; const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); platformService @@ -164,29 +174,29 @@ suite('Conda Inherit Env Prompt', async () => { settings: { globalValue: true, workspaceValue: undefined, - workspaceFolderValue: undefined - } + workspaceFolderValue: undefined, + }, }, { name: 'Returns false if workspaceValue of `terminal.integrated.inheritEnv` setting is set', settings: { globalValue: undefined, workspaceValue: true, - workspaceFolderValue: undefined - } + workspaceFolderValue: undefined, + }, }, { name: 'Returns false if workspaceFolderValue of `terminal.integrated.inheritEnv` setting is set', settings: { globalValue: undefined, workspaceValue: undefined, - workspaceFolderValue: false - } - } + workspaceFolderValue: false, + }, + }, ].forEach((testParams) => { test(testParams.name, async () => { const interpreter = { - type: InterpreterType.Conda + envType: EnvironmentType.Conda, }; const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); platformService @@ -212,12 +222,12 @@ suite('Conda Inherit Env Prompt', async () => { }); test('Returns true otherwise', async () => { const interpreter = { - type: InterpreterType.Conda + envType: EnvironmentType.Conda, }; const settings = { globalValue: undefined, workspaceValue: undefined, - workspaceFolderValue: undefined + workspaceFolderValue: undefined, }; const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); platformService @@ -244,10 +254,11 @@ suite('Conda Inherit Env Prompt', async () => { 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>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); }); teardown(() => { @@ -261,10 +272,11 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object + + platformService.object, + applicationEnvironment.object, ); const promise = condaInheritEnvPrompt.activate(resource); @@ -272,7 +284,7 @@ suite('Conda Inherit Env Prompt', async () => { await sleep(1); // Ensure activate() function has completed while initializeInBackground() is still not resolved - assert.equal(deferred.completed, true); + assert.strictEqual(deferred.completed, true); initializeInBackgroundDeferred.resolve(); await sleep(1); @@ -285,10 +297,11 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object + + platformService.object, + applicationEnvironment.object, ); await condaInheritEnvPrompt.activate(resource); assert.ok(initializeInBackground.calledOnce); @@ -301,10 +314,11 @@ suite('Conda Inherit Env Prompt', async () => { 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>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); }); teardown(() => { @@ -319,10 +333,11 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object + + platformService.object, + applicationEnvironment.object, ); await condaInheritEnvPrompt.initializeInBackground(resource); assert.ok(shouldShowPrompt.calledOnce); @@ -337,10 +352,11 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object + + platformService.object, + applicationEnvironment.object, ); await condaInheritEnvPrompt.initializeInBackground(resource); assert.ok(shouldShowPrompt.calledOnce); @@ -349,25 +365,27 @@ suite('Conda Inherit Env Prompt', async () => { }); suite('Method promptAndUpdate()', () => { - const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.moreInfo()]; + const prompts = [Common.allow, Common.close]; 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>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); when(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).thenReturn( - notificationPromptEnabled.object + notificationPromptEnabled.object, ); condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object + + platformService.object, + applicationEnvironment.object, ); }); @@ -377,7 +395,7 @@ suite('Conda Inherit Env Prompt', async () => { .returns(() => false) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -392,7 +410,7 @@ suite('Conda Inherit Env Prompt', async () => { .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); workspaceService @@ -407,16 +425,11 @@ suite('Conda Inherit Env Prompt', async () => { .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>(); @@ -425,8 +438,8 @@ suite('Conda Inherit Env Prompt', async () => { .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) - .returns(() => Promise.resolve(Common.bannerLabelYes())) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) + .returns(() => Promise.resolve(Common.allow)) .verifiable(TypeMoq.Times.once()); workspaceService .setup((ws) => ws.getConfiguration('terminal')) @@ -440,16 +453,11 @@ suite('Conda Inherit Env Prompt', async () => { .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>(); @@ -458,41 +466,8 @@ suite('Conda Inherit Env Prompt', async () => { .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())) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) + .returns(() => Promise.resolve(Common.close)) .verifiable(TypeMoq.Times.once()); workspaceService .setup((ws) => ws.getConfiguration('terminal')) @@ -505,17 +480,12 @@ suite('Conda Inherit Env Prompt', async () => { 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 deleted file mode 100644 index 62d16eec7cca..000000000000 --- a/src/test/interpreters/virtualEnvs/index.unit.test.ts +++ /dev/null @@ -1,260 +0,0 @@ -// 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 { IPipEnvService } 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(IPipEnvService))).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 index 07c628a113be..2ad67831c455 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -3,8 +3,9 @@ 'use strict'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; +import { anything, deepEqual, instance, mock, reset, verify, when } from 'ts-mockito'; import { ConfigurationTarget, Disposable, Uri } from 'vscode'; import { ApplicationShell } from '../../../client/common/application/applicationShell'; import { IApplicationShell } from '../../../client/common/application/types'; @@ -13,98 +14,161 @@ import { IPersistentState, IPersistentStateFactory } from '../../../client/commo 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 { IComponentAdapter, IInterpreterHelper, IInterpreterService } 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 { PythonInterpreter } from '../../../client/pythonEnvironments/info'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as createEnvApi from '../../../client/pythonEnvironments/creation/createEnvApi'; -// 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: PythonInterpreter, resource: Uri): Promise<void> { + + 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 componentAdapter: IComponentAdapter; + let interpreterService: IInterpreterService; let environmentPrompt: VirtualEnvironmentPromptTest; + let isCreatingEnvironmentStub: sinon.SinonStub; setup(() => { - builder = mock(InterpreterWatcherBuilder); persistentStateFactory = mock(PersistentStateFactory); helper = mock(InterpreterHelper); pythonPathUpdaterService = mock(PythonPathUpdaterService); - locator = mock(CacheableLocatorService); + componentAdapter = mock<IComponentAdapter>(); + interpreterService = mock<IInterpreterService>(); + isCreatingEnvironmentStub = sinon.stub(createEnvApi, 'isCreatingEnvironment'); + isCreatingEnvironmentStub.returns(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + id: 'selected', + path: 'path/to/selected', + } as unknown) as PythonEnvironment); disposable = mock(Disposable); appShell = mock(ApplicationShell); environmentPrompt = new VirtualEnvironmentPromptTest( - instance(builder), instance(persistentStateFactory), instance(helper), instance(pythonPathUpdaterService), - instance(locator), [instance(disposable)], - instance(appShell) + instance(appShell), + instance(componentAdapter), + instance(interpreterService), ); }); + teardown(() => { + sinon.restore(); + }); + 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 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(componentAdapter.getWorkspaceVirtualEnvInterpreters(resource)).thenResolve([ + interpreter1, + interpreter2, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(helper.getBestInterpreter(deepEqual([interpreter1, interpreter2] as any))).thenReturn(interpreter2 as any); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( - notificationPromptEnabled.object + notificationPromptEnabled.object, + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(); + + await environmentPrompt.handleNewEnvironment(resource); + + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); + }); + + test('User is not notified if currently selected interpreter is the same as new interpreter', 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>>(); + + // Return interpreters using the component adapter instead + when(componentAdapter.getWorkspaceVirtualEnvInterpreters(resource)).thenResolve([ + interpreter1, + interpreter2, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(helper.getBestInterpreter(deepEqual([interpreter1, interpreter2] as any))).thenReturn(interpreter2 as any); + reset(interpreterService); + when(interpreterService.getActiveInterpreter(anything())).thenResolve( + (interpreter2 as unknown) as PythonEnvironment, + ); + 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(appShell.showInformationMessage(anything(), ...prompts)).never(); + }); + 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>>(); + + // Return interpreters using the component adapter instead + when(componentAdapter.getWorkspaceVirtualEnvInterpreters(resource)).thenResolve([ + interpreter1, + interpreter2, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(helper.getBestInterpreter(deepEqual([interpreter1, interpreter2] as any))).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 prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( - notificationPromptEnabled.object + notificationPromptEnabled.object, ); notificationPromptEnabled.setup((n) => n.value).returns(() => true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[0] as any); when( pythonPathUpdaterService.updatePythonPath( interpreter1.path, ConfigurationTarget.WorkspaceFolder, 'ui', - resource - ) + resource, + ), ).thenResolve(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any await environmentPrompt.notifyUser(interpreter1 as any, resource); verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); @@ -114,34 +178,36 @@ suite('Virtual Environment Prompt', () => { interpreter1.path, ConfigurationTarget.WorkspaceFolder, 'ui', - resource - ) + 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 prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( - notificationPromptEnabled.object + notificationPromptEnabled.object, ); notificationPromptEnabled.setup((n) => n.value).returns(() => true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[1] as any); when( pythonPathUpdaterService.updatePythonPath( interpreter1.path, ConfigurationTarget.WorkspaceFolder, 'ui', - resource - ) + resource, + ), ).thenResolve(); notificationPromptEnabled .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any await environmentPrompt.notifyUser(interpreter1 as any, resource); verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); @@ -151,27 +217,29 @@ suite('Virtual Environment Prompt', () => { interpreter1.path, ConfigurationTarget.WorkspaceFolder, 'ui', - resource - ) + resource, + ), ).never(); notificationPromptEnabled.verifyAll(); }); - test("If user selects 'Do not show again', prompt is disabled", async () => { + test('If user selects "Don\'t 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 prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( - notificationPromptEnabled.object + notificationPromptEnabled.object, ); notificationPromptEnabled.setup((n) => n.value).returns(() => true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[2] as any); notificationPromptEnabled .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any await environmentPrompt.notifyUser(interpreter1 as any, resource); verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); @@ -182,17 +250,32 @@ suite('Virtual Environment Prompt', () => { 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 prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( - notificationPromptEnabled.object + notificationPromptEnabled.object, ); notificationPromptEnabled.setup((n) => n.value).returns(() => false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[0] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any await environmentPrompt.notifyUser(interpreter1 as any, resource); verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); verify(appShell.showInformationMessage(anything(), ...prompts)).never(); }); + + test('If environment is being created, no notification is shown', async () => { + isCreatingEnvironmentStub.reset(); + isCreatingEnvironmentStub.returns(true); + + const resource = Uri.file('a'); + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + + await environmentPrompt.handleNewEnvironment(resource); + + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).never(); + verify(appShell.showInformationMessage(anything(), ...prompts)).never(); + }); }); diff --git a/src/test/jupyter/requireJupyterPrompt.unit.test.ts b/src/test/jupyter/requireJupyterPrompt.unit.test.ts new file mode 100644 index 000000000000..0eb6c9e06958 --- /dev/null +++ b/src/test/jupyter/requireJupyterPrompt.unit.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { mock, instance, verify, anything, when } from 'ts-mockito'; +import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; +import { Commands, JUPYTER_EXTENSION_ID } from '../../client/common/constants'; +import { IDisposableRegistry } from '../../client/common/types'; +import { Common, Interpreters } from '../../client/common/utils/localize'; +import { RequireJupyterPrompt } from '../../client/jupyter/requireJupyterPrompt'; + +suite('RequireJupyterPrompt Unit Tests', () => { + let requireJupyterPrompt: RequireJupyterPrompt; + let appShell: IApplicationShell; + let commandManager: ICommandManager; + let disposables: IDisposableRegistry; + + setup(() => { + appShell = mock<IApplicationShell>(); + commandManager = mock<ICommandManager>(); + disposables = mock<IDisposableRegistry>(); + + requireJupyterPrompt = new RequireJupyterPrompt( + instance(appShell), + instance(commandManager), + instance(disposables), + ); + }); + + test('Activation registers command', async () => { + await requireJupyterPrompt.activate(); + + verify(commandManager.registerCommand(Commands.InstallJupyter, anything())).once(); + }); + + test('Show prompt with Yes selection installs Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelYes)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).once(); + }); + + test('Show prompt with No selection does not install Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelNo)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).never(); + }); +}); diff --git a/src/test/language/braceCounter.unit.test.ts b/src/test/language/braceCounter.unit.test.ts deleted file mode 100644 index 4762ae40cd2a..000000000000 --- a/src/test/language/braceCounter.unit.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index 1dfb1e91e4e2..000000000000 --- a/src/test/language/characterStream.unit.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -// 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 index 194d90638207..720d52a35476 100644 --- a/src/test/language/languageConfiguration.unit.test.ts +++ b/src/test/language/languageConfiguration.unit.test.ts @@ -3,8 +3,6 @@ 'use strict'; -// tslint:disable:max-func-body-length - import { expect } from 'chai'; import { getLanguageConfiguration } from '../../client/language/languageConfiguration'; @@ -12,8 +10,9 @@ import { getLanguageConfiguration } from '../../client/language/languageConfigur const NEEDS_INDENT = [ /^break$/, /^continue$/, - /^raise$/, // only re-raise - /^return\b/ + // raise is indented unless it's the only thing on the line + /^raise\b/, + /^return\b/, ]; const INDENT_ON_ENTER = [ // block-beginning statements @@ -30,17 +29,21 @@ const INDENT_ON_ENTER = [ /^for\b/, /^if\b/, /^elif\b/, - /^else\b/ + /^else\b/, + /^match\b/, + /^case\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/, + /// ^return\b/, /^break$/, /^continue$/, - /^raise\b/, - /^pass\b/ + // For now we are mostly ignoring "return". + // See https://github.com/microsoft/vscode-python/issues/10583. + /^raise$/, + /^pass\b/, ]; function isMember(line: string, regexes: RegExp[]): boolean { @@ -57,7 +60,7 @@ function resolveExample( leading: string, postKeyword: string, preColon: string, - trailing: string + trailing: string, ): [string | undefined, string | undefined, boolean] { let invalid: string | undefined; if (base.trim() === '') { @@ -76,12 +79,10 @@ function resolveExample( 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 { - if (resolvedBase.endsWith(':')) { - resolvedBase = `${resolvedBase.substring(0, resolvedBase.length - 1)}${postKeyword}:`; - } else { - resolvedBase = `${resolvedBase}${postKeyword}`; - } + resolvedBase = `${resolvedBase}${postKeyword}`; } } if (preColon !== '') { @@ -122,14 +123,14 @@ suite('Language Configuration', () => { 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); + // 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' + 'Multiline separator indent regex for regular strings should not have matches', ); }); @@ -137,7 +138,7 @@ suite('Language Configuration', () => { 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' + 'Multiline separator indent regex for strings with escaped characters should not have matches', ); }); @@ -145,7 +146,7 @@ suite('Language Configuration', () => { 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' + 'Multiline separator indent regex for strings with newline separator should have matches', ); }); @@ -179,6 +180,16 @@ suite('Language Configuration', () => { 'except TestError:', 'except :', 'finally:', + 'match item:', + 'case 200:', + 'case (1, 1):', + 'case Point(x=0, y=0):', + 'case [Point(0, 0)]:', + 'case Point(x, y) if x == y:', + 'case (Point(x1, y1), Point(x2, y2) as p2):', + 'case Color.RED:', + 'case 401 | 403 | 404:', + 'case _:', // simple statemenhts 'pass', 'raise Exception(msg)', @@ -198,7 +209,7 @@ suite('Language Configuration', () => { // bogus '', ' ', - ' ' + ' ', ].forEach((base) => { [ ['', '', '', ''], @@ -215,7 +226,7 @@ suite('Language Configuration', () => { // trailing ['', '', '', ' '], ['', '', '', '# a comment'], - ['', '', '', ' # ...'] + ['', '', '', ' # ...'], ].forEach((whitespace) => { const [leading, postKeyword, preColon, trailing] = whitespace; const [_example, invalid, ignored] = resolveExample(base, leading, postKeyword, preColon, trailing); diff --git a/src/test/language/textBuilder.unit.test.ts b/src/test/language/textBuilder.unit.test.ts deleted file mode 100644 index ac90a025c327..000000000000 --- a/src/test/language/textBuilder.unit.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index 34daa81534cd..000000000000 --- a/src/test/language/textIterator.unit.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index 224940f6e8b0..000000000000 --- a/src/test/language/textRange.unit.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index 53e5ff4dc650..000000000000 --- a/src/test/language/textRangeCollection.unit.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index e0ad13dc6afd..000000000000 --- a/src/test/language/tokenizer.unit.test.ts +++ /dev/null @@ -1,490 +0,0 @@ -// 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/languageServer/jediLSExtensionManager.unit.test.ts b/src/test/languageServer/jediLSExtensionManager.unit.test.ts new file mode 100644 index 000000000000..b57a0bbd096d --- /dev/null +++ b/src/test/languageServer/jediLSExtensionManager.unit.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { ILanguageServerOutputChannel } from '../../client/activation/types'; +import { IWorkspaceService, ICommandManager } from '../../client/common/application/types'; +import { IExperimentService, IConfigurationService, IInterpreterPathService } from '../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { JediLSExtensionManager } from '../../client/languageServer/jediLSExtensionManager'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; + +suite('Language Server - Jedi LS extension manager', () => { + let manager: JediLSExtensionManager; + + setup(() => { + manager = new JediLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + ); + }); + + test('Constructor should create a client proxy, a server manager and a server proxy', () => { + assert.notStrictEqual(manager.clientFactory, undefined); + assert.notStrictEqual(manager.serverManager, undefined); + }); + + test('canStartLanguageServer should return true if an interpreter is passed in', () => { + const result = manager.canStartLanguageServer(({ + path: 'path/to/interpreter', + } as unknown) as PythonEnvironment); + + assert.strictEqual(result, true); + }); + + test('canStartLanguageServer should return false otherwise', () => { + const result = manager.canStartLanguageServer(undefined); + + assert.strictEqual(result, false); + }); +}); diff --git a/src/test/languageServer/noneLSExtensionManager.unit.test.ts b/src/test/languageServer/noneLSExtensionManager.unit.test.ts new file mode 100644 index 000000000000..2f27e420ca48 --- /dev/null +++ b/src/test/languageServer/noneLSExtensionManager.unit.test.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { NoneLSExtensionManager } from '../../client/languageServer/noneLSExtensionManager'; + +suite('Language Server - No LS extension manager', () => { + let manager: NoneLSExtensionManager; + + setup(() => { + manager = new NoneLSExtensionManager(); + }); + + test('canStartLanguageServer should return true', () => { + const result = manager.canStartLanguageServer(); + + assert.strictEqual(result, true); + }); +}); diff --git a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts new file mode 100644 index 000000000000..751b26d37d3c --- /dev/null +++ b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { ILanguageServerOutputChannel } from '../../client/activation/types'; +import { IWorkspaceService, ICommandManager, IApplicationShell } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { + IExperimentService, + IConfigurationService, + IInterpreterPathService, + IExtensions, +} from '../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { PylanceLSExtensionManager } from '../../client/languageServer/pylanceLSExtensionManager'; + +suite('Language Server - Pylance LS extension manager', () => { + let manager: PylanceLSExtensionManager; + + setup(() => { + manager = new PylanceLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /** do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + {} as IExtensions, + {} as IApplicationShell, + ); + }); + + test('Constructor should create a client proxy, a server manager and a server proxy', () => { + assert.notStrictEqual(manager.clientFactory, undefined); + assert.notStrictEqual(manager.serverManager, undefined); + }); + + test('canStartLanguageServer should return true if Pylance is installed', () => { + manager = new PylanceLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /** do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => ({}), + } as unknown) as IExtensions, + {} as IApplicationShell, + ); + + const result = manager.canStartLanguageServer(); + + assert.strictEqual(result, true); + }); + + test('canStartLanguageServer should return false if Pylance is not installed', () => { + manager = new PylanceLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + } as unknown) as IExtensions, + {} as IApplicationShell, + ); + + const result = manager.canStartLanguageServer(); + + assert.strictEqual(result, false); + }); +}); diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts new file mode 100644 index 000000000000..e86e19cf2055 --- /dev/null +++ b/src/test/languageServer/watcher.unit.test.ts @@ -0,0 +1,1175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { ConfigurationChangeEvent, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { JediLanguageServerManager } from '../../client/activation/jedi/manager'; +import { NodeLanguageServerManager } from '../../client/activation/node/manager'; +import { ILanguageServerOutputChannel, LanguageServerType } from '../../client/activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { + IConfigurationService, + IDisposable, + IExperimentService, + IExtensions, + IInterpreterPathService, +} from '../../client/common/types'; +import { LanguageService } from '../../client/common/utils/localize'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { IInterpreterHelper, IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { JediLSExtensionManager } from '../../client/languageServer/jediLSExtensionManager'; +import { NoneLSExtensionManager } from '../../client/languageServer/noneLSExtensionManager'; +import { PylanceLSExtensionManager } from '../../client/languageServer/pylanceLSExtensionManager'; +import { ILanguageServerExtensionManager } from '../../client/languageServer/types'; +import { LanguageServerWatcher } from '../../client/languageServer/watcher'; +import * as Logging from '../../client/logging'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; + +suite('Language server watcher', () => { + let watcher: LanguageServerWatcher; + let disposables: IDisposable[]; + const sandbox = sinon.createSandbox(); + + setup(() => { + disposables = []; + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + + watcher.register(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('The constructor should add a listener to onDidChange to the list of disposables if it is a trusted workspace', () => { + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + {} as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + {} as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + assert.strictEqual(disposables.length, 11); + }); + + test('The constructor should not add a listener to onDidChange to the list of disposables if it is not a trusted workspace', () => { + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + {} as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: false, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + {} as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + assert.strictEqual(disposables.length, 10); + }); + + test(`When starting the language server, the language server extension manager should not be undefined`, async () => { + // First start + await watcher.startLanguageServer(LanguageServerType.None); + // get should return the None LS (the noop LS). + // This LS is returned by the None LS manager in get(). + const languageServer = await watcher.get(); + + assert.notStrictEqual(languageServer, undefined); + }); + + test(`If the interpreter changed, the existing language server should be stopped if there is one`, async () => { + const getActiveInterpreterStub = sandbox.stub(); + getActiveInterpreterStub.onFirstCall().returns('python'); + getActiveInterpreterStub.onSecondCall().returns('other/python'); + + const interpreterService = ({ + getActiveInterpreter: getActiveInterpreterStub, + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + // First start, get the reference to the extension manager. + await watcher.startLanguageServer(LanguageServerType.None); + + // For None case the object implements both ILanguageServer and ILanguageServerManager. + const extensionManager = (await watcher.get()) as ILanguageServerExtensionManager; + const stopLanguageServerSpy = sandbox.spy(extensionManager, 'stopLanguageServer'); + + // Second start, check if the first server manager was stopped and disposed of. + await watcher.startLanguageServer(LanguageServerType.None); + + assert.ok(stopLanguageServerSpy.calledOnce); + }); + + test(`When starting the language server, if the language server can be started, it should call startLanguageServer on the language server extension manager`, async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + await watcher.startLanguageServer(LanguageServerType.None); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test(`When starting the language server, if the language server can be started, there should be logs written in the output channel`, async () => { + let output = ''; + sandbox.stub(Logging, 'traceLog').callsFake((...args: unknown[]) => { + output = output.concat(...(args as string[])); + }); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => ({ folderUri: Uri.parse('workspace') }), + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(LanguageServerType.None); + + assert.strictEqual(output, LanguageService.startingNone); + }); + + test(`When starting the language server, if the language server can be started, this.languageServerType should reflect the new language server type`, async () => { + await watcher.startLanguageServer(LanguageServerType.None); + + assert.deepStrictEqual(watcher.languageServerType, LanguageServerType.None); + }); + + test(`When starting the language server, if the language server cannot be started, it should call languageServerNotAvailable`, async () => { + const canStartLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'canStartLanguageServer'); + canStartLanguageServerStub.returns(false); + const languageServerNotAvailableStub = sandbox.stub( + NoneLSExtensionManager.prototype, + 'languageServerNotAvailable', + ); + languageServerNotAvailableStub.returns(Promise.resolve()); + + await watcher.startLanguageServer(LanguageServerType.None); + + assert.ok(canStartLanguageServerStub.calledOnce); + assert.ok(languageServerNotAvailableStub.calledOnce); + }); + + test('When the config settings change, but the python.languageServer setting is not affected, the watcher should not restart the language server', async () => { + let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise<void> = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise<void>) => { + onDidChangeConfigListener = listener; + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + workspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeConfigListener({ affectsConfiguration: () => false }); + + // Check that startLanguageServer was only called once: When we called it above. + assert.ok(startLanguageServerSpy.calledOnce); + }); + + test('When the config settings change, and the python.languageServer setting is affected, the watcher should restart the language server', async () => { + let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise<void> = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise<void>) => { + onDidChangeConfigListener = listener; + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + workspaceFolders: [{ uri: Uri.parse('workspace') }], + } as unknown) as IWorkspaceService; + + const getSettingsStub = sandbox.stub(); + getSettingsStub.onFirstCall().returns({ languageServer: LanguageServerType.None }); + getSettingsStub.onSecondCall().returns({ languageServer: LanguageServerType.Node }); + + const configService = ({ + getSettings: getSettingsStub, + } as unknown) as IConfigurationService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + configService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + workspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + // Use a fake here so we don't actually start up language servers. + const startLanguageServerFake = sandbox.fake.resolves(undefined); + sandbox.replace(watcher, 'startLanguageServer', startLanguageServerFake); + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeConfigListener({ affectsConfiguration: () => true }); + + // Check that startLanguageServer was called twice: When we called it above, and implicitly because of the event. + assert.ok(startLanguageServerFake.calledTwice); + }); + + test('When starting a language server with a Python 2.7 interpreter and the python.languageServer setting is Jedi, do not instantiate a language server', async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + await watcher.startLanguageServer(LanguageServerType.Jedi); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test('When starting a language server with a Python 2.7 interpreter and the python.languageServer setting is default, use Pylance', async () => { + const startLanguageServerStub = sandbox.stub(PylanceLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + sandbox.stub(PylanceLSExtensionManager.prototype, 'canStartLanguageServer').returns(true); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ + languageServer: LanguageServerType.Jedi, + languageServerIsDefault: true, + }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(LanguageServerType.Node); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test('When starting a language server in an untrusted workspace with Jedi, do not instantiate a language server', async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: false, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(LanguageServerType.Jedi); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + [ + { + languageServer: LanguageServerType.Jedi, + multiLS: true, + extensionLSCls: JediLSExtensionManager, + lsManagerCls: JediLanguageServerManager, + }, + { + languageServer: LanguageServerType.Node, + multiLS: false, + extensionLSCls: PylanceLSExtensionManager, + lsManagerCls: NodeLanguageServerManager, + }, + { + languageServer: LanguageServerType.None, + multiLS: false, + extensionLSCls: NoneLSExtensionManager, + lsManagerCls: undefined, + }, + ].forEach(({ languageServer, multiLS, extensionLSCls, lsManagerCls }) => { + test(`When starting language servers with different resources, ${ + multiLS ? 'multiple' : 'a single' + } language server${multiLS ? 's' : ''} should be instantiated when using ${languageServer}`, async () => { + const getActiveInterpreterStub = sandbox.stub(); + getActiveInterpreterStub.onFirstCall().returns({ path: 'folder1/python', version: { major: 3, minor: 9 } }); + getActiveInterpreterStub + .onSecondCall() + .returns({ path: 'folder2/python', version: { major: 3, minor: 10 } }); + const startLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + const stopLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'stopLanguageServer'); + sandbox.stub(extensionLSCls.prototype, 'canStartLanguageServer').returns(true); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: getActiveInterpreterStub, + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(languageServer, Uri.parse('folder1')); + await watcher.startLanguageServer(languageServer, Uri.parse('folder2')); + + // If multiLS set to true, then we expect to have called startLanguageServer twice. + // If multiLS set to false, then we expect to have called startLanguageServer once. + assert.ok(startLanguageServerStub.calledTwice === multiLS); + assert.ok(startLanguageServerStub.calledOnce === !multiLS); + assert.ok(getActiveInterpreterStub.calledTwice); + assert.ok(stopLanguageServerStub.notCalled); + }); + + test(`${languageServer} language server(s) should ${ + multiLS ? '' : 'not' + } be stopped if a workspace gets removed from the current project`, async () => { + sandbox.stub(extensionLSCls.prototype, 'startLanguageServer').returns(Promise.resolve()); + if (lsManagerCls) { + sandbox.stub(lsManagerCls.prototype, 'dispose').returns(); + } + + const stopLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'stopLanguageServer'); + stopLanguageServerStub.returns(Promise.resolve()); + + let onDidChangeWorkspaceFoldersListener: (event: WorkspaceFoldersChangeEvent) => Promise<void> = () => + Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: (listener: (event: WorkspaceFoldersChangeEvent) => Promise<void>) => { + onDidChangeWorkspaceFoldersListener = listener; + }, + workspaceFolders: [{ uri: Uri.parse('workspace1') }, { uri: Uri.parse('workspace2') }], + isTrusted: true, + } as unknown) as IWorkspaceService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 3, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + workspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(languageServer, Uri.parse('workspace1')); + await watcher.startLanguageServer(languageServer, Uri.parse('workspace2')); + + await onDidChangeWorkspaceFoldersListener({ + added: [], + removed: [{ uri: Uri.parse('workspace2') } as WorkspaceFolder], + }); + + // If multiLS set to true, then we expect to have stopped a language server. + // If multiLS set to false, then we expect to not have stopped a language server. + assert.ok(stopLanguageServerStub.calledOnce === multiLS); + assert.ok(stopLanguageServerStub.notCalled === !multiLS); + }); + }); + + test('The language server should be restarted if the interpreter info changed', async () => { + const info = ({ + envPath: 'foo', + path: 'path/to/foo/bin/python', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => ({ + envPath: 'foo', + path: 'path/to/foo', + }), + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called twice: Once above, and once after the interpreter info changed. + assert.ok(startLanguageServerSpy.calledTwice); + }); + + test('The language server should not be restarted if the interpreter info did not change', async () => { + const info = ({ + envPath: 'foo', + path: 'path/to/foo', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => info, + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called once: Only when startLanguageServer() was called above. + assert.ok(startLanguageServerSpy.calledOnce); + }); + + test('The language server should not be restarted if the interpreter info changed but the env path is an empty string', async () => { + const info = ({ + envPath: '', + path: 'path/to/foo', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => ({ + envPath: 'foo', + path: 'path/to/foo', + }), + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called once: Only when startLanguageServer() was called above. + assert.ok(startLanguageServerSpy.calledOnce); + }); + + test('The language server should not be restarted if the interpreter info changed but the env path is undefined', async () => { + const info = ({ + envPath: undefined, + path: 'path/to/foo', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => ({ + envPath: 'foo', + path: 'path/to/foo', + }), + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called once: Only when startLanguageServer() was called above. + assert.ok(startLanguageServerSpy.calledOnce); + }); +}); diff --git a/src/test/languageServers/jedi/autocomplete/base.test.ts b/src/test/languageServers/jedi/autocomplete/base.test.ts deleted file mode 100644 index 8086cf18d8e2..000000000000 --- a/src/test/languageServers/jedi/autocomplete/base.test.ts +++ /dev/null @@ -1,346 +0,0 @@ -// 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 deleted file mode 100644 index 143bda870d6c..000000000000 --- a/src/test/languageServers/jedi/autocomplete/pep484.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -// 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 deleted file mode 100644 index 18ac683a2a98..000000000000 --- a/src/test/languageServers/jedi/autocomplete/pep526.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -// 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 deleted file mode 100644 index 48b1610ba944..000000000000 --- a/src/test/languageServers/jedi/completionSource.unit.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -// 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 deleted file mode 100644 index f9efcbf91dac..000000000000 --- a/src/test/languageServers/jedi/definitions/hover.jedi.test.ts +++ /dev/null @@ -1,552 +0,0 @@ -// 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 deleted file mode 100644 index 787412d0f0f8..000000000000 --- a/src/test/languageServers/jedi/definitions/navigation.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -// 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 deleted file mode 100644 index e7b612ad6fb7..000000000000 --- a/src/test/languageServers/jedi/definitions/parallel.jedi.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index 3d4b87ad9dda..000000000000 --- a/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -// 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 deleted file mode 100644 index c6ceb65e1867..000000000000 --- a/src/test/languageServers/jedi/signature/signature.jedi.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// 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 deleted file mode 100644 index 72235e0f4e3c..000000000000 --- a/src/test/languageServers/jedi/symbolProvider.unit.test.ts +++ /dev/null @@ -1,461 +0,0 @@ -// 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/legacyFileSystem.ts b/src/test/legacyFileSystem.ts new file mode 100644 index 000000000000..7584f9619943 --- /dev/null +++ b/src/test/legacyFileSystem.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { FileSystem, FileSystemUtils, RawFileSystem } from '../client/common/platform/fileSystem'; +import { FakeVSCodeFileSystemAPI } from './fakeVSCFileSystemAPI'; + +export class LegacyFileSystem extends FileSystem { + constructor() { + super(); + const vscfs = new FakeVSCodeFileSystemAPI(); + const raw = RawFileSystem.withDefaults(undefined, vscfs); + this.utils = FileSystemUtils.withDefaults(raw); + } +} diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts deleted file mode 100644 index 4c5d03f066ef..000000000000 --- a/src/test/linters/common.ts +++ /dev/null @@ -1,342 +0,0 @@ -// 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 deleted file mode 100644 index 0cac56804c11..000000000000 --- a/src/test/linters/lint.args.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -// 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 deleted file mode 100644 index 7d7f30676cb6..000000000000 --- a/src/test/linters/lint.functional.test.ts +++ /dev/null @@ -1,855 +0,0 @@ -// 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 deleted file mode 100644 index be23cef27d9b..000000000000 --- a/src/test/linters/lint.manager.unit.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -// 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 deleted file mode 100644 index 20d3ea5a54f6..000000000000 --- a/src/test/linters/lint.multilinter.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -// 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: string = - '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: string = - "************* 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, workspaceUri); - 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 deleted file mode 100644 index f7dcebd731f5..000000000000 --- a/src/test/linters/lint.multiroot.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index d5f9d3f3220a..000000000000 --- a/src/test/linters/lint.provider.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -// 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 deleted file mode 100644 index 71b3c694bab4..000000000000 --- a/src/test/linters/lint.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -// 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 deleted file mode 100644 index 7c92fac4c0cc..000000000000 --- a/src/test/linters/lint.unit.test.ts +++ /dev/null @@ -1,818 +0,0 @@ -// 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 deleted file mode 100644 index e2db5059dfab..000000000000 --- a/src/test/linters/lintengine.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -// 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 deleted file mode 100644 index 23e11ec8e9cc..000000000000 --- a/src/test/linters/linter.availability.unit.test.ts +++ /dev/null @@ -1,861 +0,0 @@ -// 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 deleted file mode 100644 index 4f7259fd41d7..000000000000 --- a/src/test/linters/linterCommands.unit.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -// 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 deleted file mode 100644 index efdc927eaa9d..000000000000 --- a/src/test/linters/linterManager.unit.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -// 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 deleted file mode 100644 index a648c1949d46..000000000000 --- a/src/test/linters/linterinfo.unit.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -// 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 deleted file mode 100644 index b0d0843a9173..000000000000 --- a/src/test/linters/mypy.unit.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 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 deleted file mode 100644 index 02b19a7ef9bd..000000000000 --- a/src/test/linters/pylint.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -// 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 deleted file mode 100644 index b09bc1feecb3..000000000000 --- a/src/test/linters/pylint.unit.test.ts +++ /dev/null @@ -1,537 +0,0 @@ -// 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 deleted file mode 100644 index 4f63e9fae940..000000000000 --- a/src/test/linters/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 20cd05e55a93..000000000000 --- a/src/test/markdown/restTextConverter.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -// 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 index cca3df5787bc..e2de7e649b87 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -1,19 +1,32 @@ import * as vscode from 'vscode'; -import { - Flake8CategorySeverity, - ILintingSettings, - IMypyCategorySeverity, - IPycodestyleCategorySeverity, - IPylintCategorySeverity -} from '../client/common/types'; +import * as util from 'util'; -export class MockOutputChannel implements vscode.OutputChannel { +export class MockOutputChannel implements vscode.LogOutputChannel { public name: string; public output: string; public isShown!: boolean; + private _eventEmitter = new vscode.EventEmitter<vscode.LogLevel>(); + public onDidChangeLogLevel: vscode.Event<vscode.LogLevel> = this._eventEmitter.event; constructor(name: string) { this.name = name; this.output = ''; + this.logLevel = vscode.LogLevel.Debug; + } + public logLevel: vscode.LogLevel; + trace(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + debug(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + info(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + warn(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + error(error: string | Error, ...args: any[]): void { + this.appendLine(util.format(error, ...args)); } public append(value: string) { this.output += value; @@ -22,68 +35,40 @@ export class MockOutputChannel implements vscode.OutputChannel { this.append(value); this.append('\n'); } - // tslint:disable-next-line:no-empty + + public replace(value: string): void { + this.output = value; + } + 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 { + backgroundColor: vscode.ThemeColor | undefined; + accessibilityInformation: vscode.AccessibilityInformation | undefined; 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 id: string = ''; + public name: string = ''; + 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; + public dispose(): void {} } diff --git a/src/test/mocks/autoSelector.ts b/src/test/mocks/autoSelector.ts index c0b6130bbd73..cc4ab4ddb8e5 100644 --- a/src/test/mocks/autoSelector.ts +++ b/src/test/mocks/autoSelector.ts @@ -8,29 +8,36 @@ import { Event, EventEmitter } from 'vscode'; import { Resource } from '../../client/common/types'; import { IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService + IInterpreterAutoSelectionProxyService, } from '../../client/interpreter/autoSelection/types'; -import { PythonInterpreter } from '../../client/pythonEnvironments/info'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; @injectable() export class MockAutoSelectionService - implements IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService { - public async setWorkspaceInterpreter(_resource: Resource, _interpreter: PythonInterpreter): Promise<void> { + implements IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService { + // eslint-disable-next-line class-methods-use-this + public async setWorkspaceInterpreter(_resource: Resource, _interpreter: PythonEnvironment): Promise<void> { return Promise.resolve(); } - public async setGlobalInterpreter(_interpreter: PythonInterpreter): Promise<void> { - return; - } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public async setGlobalInterpreter(_interpreter: PythonEnvironment): Promise<void> {} + + // eslint-disable-next-line class-methods-use-this get onDidChangeAutoSelectedInterpreter(): Event<void> { return new EventEmitter<void>().event; } + + // eslint-disable-next-line class-methods-use-this public autoSelectInterpreter(_resource: Resource): Promise<void> { return Promise.resolve(); } - public getAutoSelectedInterpreter(_resource: Resource): PythonInterpreter | undefined { - return; - } - public registerInstance(_instance: IInterpreterAutoSeletionProxyService): void { - return; + + // eslint-disable-next-line class-methods-use-this + public getAutoSelectedInterpreter(_resource: Resource): PythonEnvironment | undefined { + return undefined; } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public registerInstance(_instance: IInterpreterAutoSelectionProxyService): void {} } diff --git a/src/test/mocks/extension.ts b/src/test/mocks/extension.ts new file mode 100644 index 000000000000..61d70eb5ee9e --- /dev/null +++ b/src/test/mocks/extension.ts @@ -0,0 +1,16 @@ +import { injectable } from 'inversify'; +import { Extension, ExtensionKind, Uri } from 'vscode'; + +@injectable() +export class MockExtension<T> implements Extension<T> { + id!: string; + extensionUri!: Uri; + extensionPath!: string; + isActive!: boolean; + packageJSON: any; + extensionKind!: ExtensionKind; + exports!: T; + activate(): Thenable<T> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts new file mode 100644 index 000000000000..efe9b6b8ca31 --- /dev/null +++ b/src/test/mocks/extensions.ts @@ -0,0 +1,23 @@ +import { injectable } from 'inversify'; +import { IExtensions } from '../../client/common/types'; +import { Extension, Event } from 'vscode'; +import { MockExtension } from './extension'; + +@injectable() +export class MockExtensions implements IExtensions { + extensionIdsToFind: unknown[] = []; + all: readonly Extension<unknown>[] = []; + onDidChange: Event<void> = () => { + throw new Error('Method not implemented'); + }; + getExtension(extensionId: string): Extension<unknown> | undefined; + getExtension<T>(extensionId: string): Extension<T> | undefined; + getExtension(extensionId: unknown): import('vscode').Extension<unknown> | undefined { + if (this.extensionIdsToFind.includes(extensionId)) { + return new MockExtension(); + } + } + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/helper.ts b/src/test/mocks/helper.ts new file mode 100644 index 000000000000..d61bf728a25c --- /dev/null +++ b/src/test/mocks/helper.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as TypeMoq from 'typemoq'; +import { Readable } from 'stream'; +// eslint-disable-next-line import/no-unresolved +import * as common from 'typemoq/Common/_all'; + +export class FakeReadableStream extends Readable { + _read(_size: unknown): void | null { + // custom reading logic here + this.push(null); // end the stream + } +} + +export function createTypeMoq<T>( + targetCtor?: common.CtorWithArgs<T>, + behavior?: TypeMoq.MockBehavior, + shouldOverrideTarget?: boolean, + ...targetCtorArgs: any[] +): 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>(targetCtor, behavior, shouldOverrideTarget, ...targetCtorArgs); + result.setup((x: any) => x.then).returns(() => undefined); + return result; +} diff --git a/src/test/mocks/mementos.ts b/src/test/mocks/mementos.ts index 77fde4c8e505..1ffa09884262 100644 --- a/src/test/mocks/mementos.ts +++ b/src/test/mocks/mementos.ts @@ -6,21 +6,31 @@ 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 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _value: Record<string, any> = {}; + + public keys(): string[] { + return Object.keys(this._value); + } + + // @ts-ignore Ignore the return value warning + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-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 + + // eslint-disable-next-line @typescript-eslint/no-explicit-any return exists ? this._value[key] : (defaultValue! as any); } - // tslint:disable-next-line:no-any + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any public update(key: string, value: any): Thenable<void> { this._value[key] = value; return Promise.resolve(); } - public clear() { + + public clear(): void { this._value = {}; } } diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 000000000000..e26ea1c7aa45 --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,243 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serializable, SendHandle, MessageOptions } from 'child_process'; +import { EventEmitter } from 'node:events'; +import { Writable, Readable, Pipe } from 'stream'; +import { FakeReadableStream } from './helper'; + +export class MockChildProcess extends EventEmitter { + constructor(spawnfile: string, spawnargs: string[]) { + super(); + this.spawnfile = spawnfile; + this.spawnargs = spawnargs; + this.stdin = new Writable(); + this.stdout = new FakeReadableStream(); + this.stderr = new FakeReadableStream(); + this.channel = null; + this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; + this.killed = false; + this.connected = false; + this.exitCode = null; + this.signalCode = null; + this.eventMap = new Map(); + } + + stdin: Writable | null; + + stdout: Readable | null; + + stderr: Readable | null; + + eventMap: Map<string, any>; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (...arg0: unknown[]) => void) => { + const argsArray: unknown[] = Array.isArray(args) ? args : [args]; + listener(...argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array<any> { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } + + dispose(): void { + this.stdout?.destroy(); + } +} diff --git a/src/test/datascience/mockDocument.ts b/src/test/mocks/mockDocument.ts similarity index 77% rename from src/test/datascience/mockDocument.ts rename to src/test/mocks/mockDocument.ts index f19264910e0c..a9cd39985311 100644 --- a/src/test/datascience/mockDocument.ts +++ b/src/test/mocks/mockDocument.ts @@ -1,19 +1,18 @@ +/* eslint-disable max-classes-per-file */ // 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'; +import { EndOfLine, Position, Range, TextDocument, TextDocumentContentChangeEvent, TextLine, Uri } from 'vscode'; 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) { @@ -24,24 +23,30 @@ class MockLine implements TextLine { 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; @@ -52,26 +57,41 @@ class MockLine implements TextLine { export class MockDocument implements TextDocument { private _uri: Uri; - private _version: number = 0; + + private _version = 0; + private _lines: MockLine[] = []; - private _contents: string = ''; + + private _contents = ''; + private _isUntitled = false; + private _isDirty = false; + + private _language = 'python'; + private _onSave: (doc: TextDocument) => Promise<boolean>; - constructor(contents: string, fileName: string, onSave: (doc: TextDocument) => Promise<boolean>) { + constructor( + contents: string, + fileName: string, + onSave: (doc: TextDocument) => Promise<boolean>, + language?: string, + ) { this._uri = Uri.file(fileName); this._contents = contents; this._lines = this.createLines(); this._onSave = onSave; + this._language = language ?? this._language; } + encoding: string = 'utf8'; - public setContent(contents: string) { + public setContent(contents: string): void { this._contents = contents; this._lines = this.createLines(); } - public addContent(contents: string) { + public addContent(contents: string): void { this.setContent(`${this._contents}\n${contents}`); } @@ -83,6 +103,7 @@ export class MockDocument implements TextDocument { public get uri(): Uri { return this._uri; } + public get fileName(): string { return this._uri.fsPath; } @@ -90,37 +111,48 @@ export class MockDocument implements TextDocument { public get isUntitled(): boolean { return this._isUntitled; } + public get languageId(): string { - return 'python'; + return this._language; } + public get version(): number { return this._version; } + public get isDirty(): boolean { return this._isDirty; } + + // eslint-disable-next-line class-methods-use-this public get isClosed(): boolean { return false; } + public save(): Thenable<boolean> { return this._onSave(this); } + + // eslint-disable-next-line class-methods-use-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]; } + 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; @@ -132,42 +164,32 @@ export class MockDocument implements TextDocument { } 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); } + const startOffset = this.convertToOffset(range.start); + const endOffset = this.convertToOffset(range.end); + return this._contents.substr(startOffset, endOffset - startOffset); } + + // eslint-disable-next-line class-methods-use-this public getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined { - if (!regexp) { + if (!regexp && position.line > 0) { // 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; + regexp = /a/; } - 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; } + + // eslint-disable-next-line class-methods-use-this public validateRange(range: Range): Range { return range; } + + // eslint-disable-next-line class-methods-use-this public validatePosition(position: Position): Position { return position; } @@ -190,11 +212,12 @@ export class MockDocument implements TextDocument { }); } + // eslint-disable-next-line class-methods-use-this private createTextLine(line: string, index: number, prevLine: MockLine | undefined): MockLine { return new MockLine( line, index, - prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0 + prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0, ); } diff --git a/src/test/datascience/mockDocumentManager.ts b/src/test/mocks/mockDocumentManager.ts similarity index 84% rename from src/test/datascience/mockDocumentManager.ts rename to src/test/mocks/mockDocumentManager.ts index 20e711c1d9d4..43134fb5fc02 100644 --- a/src/test/datascience/mockDocumentManager.ts +++ b/src/test/mocks/mockDocumentManager.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; + import * as path from 'path'; import { DecorationRenderOptions, @@ -17,94 +19,120 @@ import { TextEditorViewColumnChangeEvent, Uri, ViewColumn, - WorkspaceEdit + 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'; +import { IMockDocumentManager } from './mockTypes'; -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length - -export class MockDocumentManager implements IDocumentManager { +export class MockDocumentManager implements IMockDocumentManager { 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 + _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); + + public showTextDocument(document: unknown, _column?: unknown, _preserveFocus?: unknown): Thenable<TextEditor> { + this.visibleTextEditors.push(document as TextEditor); 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); + + public openTextDocument(_options?: unknown): Thenable<TextDocument> { + const opts = _options as { content?: string }; + if (opts && opts.content) { + const doc = new MockDocument(opts.content, 'Untitled-1', this.saveDocument); this.textDocuments.push(doc); } return Promise.resolve(this.lastDocument); } + + // eslint-disable-next-line class-methods-use-this public applyEdit(_edit: WorkspaceEdit): Thenable<boolean> { throw new Error('Method not implemented.'); } - public addDocument(code: string, file: string) { + public addDocument(code: string, file: string, language?: string): MockDocument { 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); + existing = new MockDocument(code, file, this.saveDocument, language); this.textDocuments.push(existing); } return existing; } - public changeDocument(file: string, changes: { range: Range; newText: string }[]) { + public changeDocument(file: string, changes: { range: Range; newText: string }[]): void { const doc = this.textDocuments.find((d) => d.uri.fsPath === Uri.file(file).fsPath) as MockDocument; if (doc) { const contentChanges = changes.map((c) => { @@ -114,12 +142,13 @@ export class MockDocumentManager implements IDocumentManager { range: c.range, rangeOffset: startOffset, rangeLength: endOffset - startOffset, - text: c.newText + text: c.newText, }; }); const ev: TextDocumentChangeEvent = { document: doc, - contentChanges + contentChanges, + reason: undefined, }; // Changes are applied to the doc before it's sent. ev.contentChanges.forEach(doc.edit.bind(doc)); @@ -127,6 +156,7 @@ export class MockDocumentManager implements IDocumentManager { } } + // eslint-disable-next-line class-methods-use-this public createTextEditorDecorationType(_options: DecorationRenderOptions): TextEditorDecorationType { throw new Error('Method not implemented'); } diff --git a/src/test/mocks/mockTextEditor.ts b/src/test/mocks/mockTextEditor.ts new file mode 100644 index 000000000000..6c1c91f45577 --- /dev/null +++ b/src/test/mocks/mockTextEditor.ts @@ -0,0 +1,134 @@ +/* eslint-disable max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { + DecorationOptions, + EndOfLine, + Position, + Range, + Selection, + SnippetString, + TextDocument, + TextEditorDecorationType, + TextEditorEdit, + TextEditorOptions, + TextEditorRevealType, + ViewColumn, +} from 'vscode'; + +import { noop } from '../../client/common/utils/misc'; +import { MockDocument } from './mockDocument'; +import { IMockDocumentManager, IMockTextEditor } from './mockTypes'; + +class MockEditorEdit implements TextEditorEdit { + constructor(private _documentManager: IMockDocumentManager, 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, + }, + ]); + } + + // eslint-disable-next-line class-methods-use-this + public delete(_location: Selection | Range): void { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + public setEndOfLine(_endOfLine: EndOfLine): void { + throw new Error('Method not implemented.'); + } +} + +export class MockEditor implements IMockTextEditor { + public selection: Selection; + + public selections: Selection[] = []; + + private _revealCallback: () => void; + + constructor(private _documentManager: IMockDocumentManager, private _document: MockDocument) { + this.selection = new Selection(0, 0, 0, 0); + this._revealCallback = noop; + } + + public get document(): TextDocument { + return this._document; + } + + // eslint-disable-next-line class-methods-use-this + public get visibleRanges(): Range[] { + return []; + } + + // eslint-disable-next-line class-methods-use-this + public get options(): TextEditorOptions { + return {}; + } + + // eslint-disable-next-line class-methods-use-this + 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); + }); + } + + // eslint-disable-next-line class-methods-use-this + public insertSnippet( + _snippet: SnippetString, + _location?: Range | Position | Range[] | Position[] | undefined, + _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined, + ): Thenable<boolean> { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + public setDecorations( + _decorationType: TextEditorDecorationType, + _rangesOrOptions: Range[] | DecorationOptions[], + ): void { + throw new Error('Method not implemented.'); + } + + public revealRange(_range: Range, _revealType?: TextEditorRevealType | undefined): void { + this._revealCallback(); + } + + // eslint-disable-next-line class-methods-use-this + public show(_column?: ViewColumn | undefined): void { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + public hide(): void { + throw new Error('Method not implemented.'); + } + + public setRevealCallback(callback: () => void): void { + this._revealCallback = callback; + } +} diff --git a/src/test/mocks/mockTypes.ts b/src/test/mocks/mockTypes.ts new file mode 100644 index 000000000000..eb560efcef99 --- /dev/null +++ b/src/test/mocks/mockTypes.ts @@ -0,0 +1,8 @@ +import { Range, TextEditor } from 'vscode'; +import { IDocumentManager } from '../../client/common/application/types'; + +export interface IMockTextEditor extends TextEditor {} + +export interface IMockDocumentManager extends IDocumentManager { + changeDocument(file: string, changes: { range: Range; newText: string }[]): void; +} diff --git a/src/test/mocks/mockWorkspaceConfig.ts b/src/test/mocks/mockWorkspaceConfig.ts new file mode 100644 index 000000000000..8627cd599fba --- /dev/null +++ b/src/test/mocks/mockWorkspaceConfig.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ConfigurationTarget, WorkspaceConfiguration } from 'vscode'; + +type SectionType<T> = { + key: string; + defaultValue?: T | undefined; + globalValue?: T | undefined; + globalLanguageValue?: T | undefined; + workspaceValue?: T | undefined; + workspaceLanguageValue?: T | undefined; + workspaceFolderValue?: T | undefined; + workspaceFolderLanguageValue?: T | undefined; +}; + +export class MockWorkspaceConfiguration implements WorkspaceConfiguration { + private values = new Map<string, unknown>(); + + constructor(defaultSettings?: { [key: string]: unknown }) { + if (defaultSettings) { + const keys = [...Object.keys(defaultSettings)]; + keys.forEach((k) => this.values.set(k, defaultSettings[k])); + } + } + + public get<T>(key: string, defaultValue?: T): T | undefined { + if (this.values.has(key)) { + return this.values.get(key) as T; + } + + return arguments.length > 1 ? defaultValue : undefined; + } + + public has(section: string): boolean { + return this.values.has(section); + } + + public inspect<T>(section: string): SectionType<T> | undefined { + return this.values.get(section) as SectionType<T>; + } + + public update( + section: string, + value: unknown, + _configurationTarget?: boolean | ConfigurationTarget | undefined, + ): Promise<void> { + this.values.set(section, value); + return Promise.resolve(); + } +} diff --git a/src/test/mocks/moduleInstaller.ts b/src/test/mocks/moduleInstaller.ts index 68e3444d14a5..fb183e6ebd99 100644 --- a/src/test/mocks/moduleInstaller.ts +++ b/src/test/mocks/moduleInstaller.ts @@ -1,22 +1,33 @@ import { EventEmitter } from 'events'; import { Uri } from 'vscode'; import { IModuleInstaller } from '../../client/common/installer/types'; +import { Product } from '../../client/common/types'; +import { ModuleInstallerType } from '../../client/pythonEnvironments/info'; export class MockModuleInstaller extends EventEmitter implements IModuleInstaller { constructor(public readonly displayName: string, private supported: boolean) { super(); } + // eslint-disable-next-line class-methods-use-this public get name(): string { return 'mock'; } + // eslint-disable-next-line class-methods-use-this + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pip; + } + + // eslint-disable-next-line class-methods-use-this public get priority(): number { return 0; } - public async installModule(name: string, _resource?: Uri): Promise<void> { + + public async installModule(name: Product | 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 index 59f069cd4d33..17cb71fb5922 100644 --- a/src/test/mocks/proc.ts +++ b/src/test/mocks/proc.ts @@ -3,13 +3,14 @@ import 'rxjs/add/observable/of'; import { EventEmitter } from 'events'; import { Observable } from 'rxjs/Observable'; +import { ChildProcess } from 'child_process'; import { ExecutionResult, IProcessService, ObservableExecutionResult, Output, ShellOptions, - SpawnOptions + SpawnOptions, } from '../../client/common/process/types'; import { noop } from '../core'; @@ -22,11 +23,13 @@ export class MockProcessService extends EventEmitter implements IProcessService constructor(private procService: IProcessService) { super(); } + public onExecObservable( - handler: (file: string, args: string[], options: SpawnOptions, callback: ExecObservableCallback) => void - ) { + handler: (file: string, args: string[], options: SpawnOptions, callback: ExecObservableCallback) => void, + ): 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; @@ -39,30 +42,30 @@ export class MockProcessService extends EventEmitter implements IProcessService const output = value as Output<string>; if (['stderr', 'stdout'].some((source) => source === output.source)) { return { - // tslint:disable-next-line:no-any - proc: {} as any, + proc: {} as ChildProcess, 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); + return { + proc: {} as ChildProcess, + out: value as Observable<Output<string>>, + dispose: () => { + noop(); + }, + }; } + return this.procService.execObservable(file, args, options); } - public onExec(handler: (file: string, args: string[], options: SpawnOptions, callback: ExecCallback) => void) { + + public onExec( + handler: (file: string, args: string[], options: SpawnOptions, callback: ExecCallback) => void, + ): 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; @@ -85,7 +88,6 @@ export class MockProcessService extends EventEmitter implements IProcessService return valueReturned ? value! : this.procService.shellExec(command, options); } - public dispose() { - return; - } + // eslint-disable-next-line @typescript-eslint/no-empty-function + public dispose(): void {} } diff --git a/src/test/mocks/process.ts b/src/test/mocks/process.ts index 1d92693d8923..d290cae5bf71 100644 --- a/src/test/mocks/process.ts +++ b/src/test/mocks/process.ts @@ -1,28 +1,38 @@ // 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'; +import { createTypeMoq } from './helper'; @injectable() export class MockProcess implements ICurrentProcess { constructor(public env: EnvironmentVariables = { ...process.env }) {} + + // eslint-disable-next-line @typescript-eslint/ban-types public on(_event: string | symbol, _listener: Function): this { return this; } + + // eslint-disable-next-line class-methods-use-this public get argv(): string[] { return []; } + + // eslint-disable-next-line class-methods-use-this public get stdout(): NodeJS.WriteStream { - return TypeMoq.Mock.ofType<NodeJS.WriteStream>().object; + return createTypeMoq<NodeJS.WriteStream>().object; } + + // eslint-disable-next-line class-methods-use-this public get stdin(): NodeJS.ReadStream { - return TypeMoq.Mock.ofType<NodeJS.ReadStream>().object; + return createTypeMoq<NodeJS.ReadStream>().object; } + // eslint-disable-next-line class-methods-use-this public get execPath(): string { return ''; } diff --git a/src/test/mocks/vsc/arrays.ts b/src/test/mocks/vsc/arrays.ts index 9291745a066f..ad2020c57110 100644 --- a/src/test/mocks/vsc/arrays.ts +++ b/src/test/mocks/vsc/arrays.ts @@ -1,415 +1,399 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + 'use strict'; -// tslint:disable:all +/** + * 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 = 0): T { + return array[array.length - (1 + n)]; +} -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; } - export function equals<T>(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { - if (one.length !== other.length) { + for (let i = 0, len = one.length; i < len; i += 1) { + if (!itemEquals(one[i], other[i])) { return false; } + } - for (let i = 0, len = one.length; i < len; i++) { - if (!itemEquals(one[i], other[i])) { - return false; - } - } + return true; +} - return true; +export function binarySearch<T>(array: T[], key: T, comparator: (op1: T, op2: T) => number): number { + let low = 0; + let high = array.length - 1; + + while (low <= high) { + const mid = ((low + high) / 2) | 0; + const comp = comparator(array[mid], key); + if (comp < 0) { + low = mid + 1; + } else if (comp > 0) { + high = mid - 1; + } else { + return mid; + } } + return -(low + 1); +} - 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; - } +/** + * 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; + let high = array.length; + if (high === 0) { + return 0; // no children + } + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (p(array[mid])) { + high = mid; + } else { + low = mid + 1; } - return -(low + 1); } + return low; +} - /** - * 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; - } +/** + * 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) { + const ret = compare(left[leftIdx], right[rightIdx]); + if (ret <= 0) { + // smaller_equal -> take left to preserve order + data[(i += 1)] = left[(leftIdx += 1)]; + } else { + // greater -> take right + data[(i += 1)] = right[(rightIdx += 1)]; } - return low; } + while (leftIdx < left.length) { + data[(i += 1)] = left[(leftIdx += 1)]; + } + while (rightIdx < right.length) { + data[(i += 1)] = right[(rightIdx += 1)]; + } +} + +export function groupBy<T>(data: T[], compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined; - /** - * 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; + for (const element of mergeSort(data.slice(0), compare)) { + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } } + return result; +} - function _divideAndMerge<T>(data: T[], compare: (a: T, b: T) => number): void { - if (data.length <= 1) { - // sorted +type IMutableSplice<T> = { + deleteCount: number; + start: number; + toInsert: T[]; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +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 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++]; + + 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 }); } } - 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); - } + let beforeIdx = 0; + let afterIdx = 0; + + while (beforeIdx !== before.length || afterIdx !== after.length) { + 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; } - 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 }); - } - } + if (beforeIdx === before.length) { + pushSplice(beforeIdx, 0, after.slice(afterIdx)); + } else if (afterIdx === after.length) { + pushSplice(beforeIdx, before.length - beforeIdx, []); + } - 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; +} - return result; +/** + * Takes two *sorted* arrays and computes their delta (removed, added elements). + * Finishes in `Math.min(before.length, after.length)` steps. + */ +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); } - /** - * 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 }; +} - 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; +} - /** - * 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 []; +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 += 1) { + 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); } - 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; } - /** - * @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); +} - return array.filter((e) => !!e); - } +/** + * Moves the element in the array for the provided positions. + */ +export function move(array: unknown[], from: number, to: number): void { + array.splice(to, 0, array.splice(from, 1)[0]); +} - /** - * 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: unknown): boolean { + return !Array.isArray(obj) || (<Array<unknown>>obj).length === 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) => array.indexOf(element) === position); } - /** - * 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; } - const seen: Record<string, boolean> = Object.create(null); - return array.filter((elem) => { - const key = keyFn(elem); - if (seen[key]) { - return false; - } + seen[key] = true; - seen[key] = true; + return true; + }); +} - return true; - }); - } +export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean { + const seen: Record<string, boolean> = Object.create(null); - 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); - return (element) => { - const key = keyFn(element); + if (seen[key]) { + return false; + } + + seen[key] = true; + return true; + }; +} - if (seen[key]) { - return false; - } +export function firstIndex<T>(array: T[], fn: (item: T) => boolean): number { + for (let i = 0; i < array.length; i += 1) { + const element = array[i]; - seen[key] = true; - return true; - }; + if (fn(element)) { + return i; + } } - export function firstIndex<T>(array: T[], fn: (item: T) => boolean): number { - for (let i = 0; i < array.length; i++) { - const element = array[i]; + return -1; +} - if (fn(element)) { - return i; - } - } +export function first<T>(array: T[], fn: (item: T) => boolean, notFoundValue: T | null = null): T { + const idx = firstIndex(array, fn); + return idx < 0 && notFoundValue !== null ? notFoundValue : array[idx]; +} - 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[], eqls: (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 && eqls(one[i], other[i]); i += 1) { + result += 1; } - export function commonPrefixLength<T>( - one: T[], - other: T[], - equals: (a: T, b: T) => boolean = (a, b) => a === b - ): number { - let result = 0; + return result; +} - for (let i = 0, len = Math.min(one.length, other.length); i < len && equals(one[i], other[i]); i++) { - result++; - } +export function flatten<T>(arr: T[][]): T[] { + return ([] as T[]).concat(...arr); +} - return result; - } +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; - export function flatten<T>(arr: T[][]): T[] { - // @ts-ignore - return [].concat(...arr); + if (typeof to === 'number') { + from = arg; + } else { + from = 0; + to = arg; } - 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; + const result: number[] = []; - if (typeof to === 'number') { - from = arg; - } else { - from = 0; - to = arg; + if (from <= to) { + for (let i = from; i < to; i += 1) { + result.push(i); } - - 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); - } + } else { + for (let i = from; i > to; i -= 1) { + 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 result; +} - return arr; +export function fill<T>(num: number, valueFn: () => T, arr: T[] = []): T[] { + for (let i = 0; i < num; i += 1) { + arr[i] = valueFn(); } - 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)); - } + return arr; +} - /** - * 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); - } - }; - } +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 unknown) as R, +): Record<string, R> { + return array.reduce((r, t) => { + const key = indexer(t); + r[key] = merger(t, r[key]); + return r; + }, Object.create(null)); +} - /** - * 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); - } +/** + * 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 idx = array.indexOf(element); + if (idx > -1) { + array.splice(idx, 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 index b13bc31658d1..fe450d491ef1 100644 --- a/src/test/mocks/vsc/charCode.ts +++ b/src/test/mocks/vsc/charCode.ts @@ -1,10 +1,9 @@ +/* eslint-disable camelcase */ /*--------------------------------------------------------------------------------------------- * 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/ /** @@ -347,8 +346,8 @@ export const enum CharCode { 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_CIRCUMFLEX = Caret, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = BackTick, // U+0060 GRAVE ACCENT U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS U_MACRON = 0x00af, // U+00AF MACRON U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT @@ -422,5 +421,5 @@ export const enum CharCode { * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) * http://www.fileformat.info/info/unicode/char/feff/index.htm */ - UTF8_BOM = 65279 + UTF8_BOM = 65279, } diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts index e5ca75125f05..c2c1188c3449 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -1,2117 +1,2352 @@ -/*--------------------------------------------------------------------------------------------- - * 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'; +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -// tslint:disable:all +'use strict'; import { relative } from 'path'; import * as vscode from 'vscode'; -import { vscMockHtmlContent } from './htmlContent'; -import { vscMockStrings } from './strings'; -import { vscUri } from './uri'; +import * as vscMockHtmlContent from './htmlContent'; +import * as vscMockStrings from './strings'; +import * as vscUri from './uri'; import { generateUuid } from './uuid'; -export namespace vscMockExtHostedTypes { - export enum CellKind { - Markdown = 1, - Code = 2 - } +export enum NotebookCellKind { + Markup = 1, + Code = 2, +} - export enum CellOutputKind { - Text = 1, - Error = 2, - Rich = 3 - } - export enum NotebookCellRunState { - Running = 1, - Idle = 2, - Success = 3, - Error = 4 - } +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; - } +export interface IRelativePattern { + base: string; + pattern: string; + pathToRelative(from: string, to: string): string; +} - // tslint:disable:all - const illegalArgument = (msg = 'Illegal Argument') => new Error(msg); +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(); - } +export class Disposable { + static from(...disposables: { dispose(): () => void }[]): Disposable { + return new Disposable(() => { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); } - // @ts-ignore - disposables = undefined; } - }); - } - private _callOnDispose: Function; + disposables = []; + } + }); + } - constructor(callOnDispose: Function) { - this._callOnDispose = callOnDispose; - } + private _callOnDispose: (() => void) | undefined; - dispose(): any { - if (typeof this._callOnDispose === 'function') { - this._callOnDispose(); - // @ts-ignore - this._callOnDispose = undefined; - } + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; + } + + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + 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; - } +export class Position { + static Min(...positions: Position[]): Position { + let result = positions.pop(); + for (const p of positions) { + if (result && p.isBefore(result)) { + result = p; } - // @ts-ignore - return result; } + return result || new Position(0, 0); + } - static Max(...positions: Position[]): Position { - let result = positions.pop(); - for (let p of positions) { - // @ts-ignore - if (p.isAfter(result)) { - result = p; - } + static Max(...positions: Position[]): Position { + let result = positions.pop(); + for (const p of positions) { + if (result && p.isAfter(result)) { + result = p; } - // @ts-ignore - return result; } + return result || new Position(0, 0); + } - 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; - } + static isPosition(other: unknown): other is Position { + if (!other) { return false; } + if (other instanceof Position) { + return true; + } + const { line, character } = <Position>other; + if (typeof line === 'number' && typeof character === 'number') { + return true; + } + return false; + } - private _line: number; - private _character: number; + private _line: number; - get line(): number { - return this._line; - } + private _character: number; - get character(): number { - return this._character; - } + get line(): number { + return this._line; + } - 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; - } + get character(): number { + return this._character; + } - isBefore(other: Position): boolean { - if (this._line < other._line) { - return true; - } - if (other._line < this._line) { - return false; - } - return this._character < other._character; + constructor(line: number, character: number) { + if (line < 0) { + throw illegalArgument('line must be non-negative'); } - - isBeforeOrEqual(other: Position): boolean { - if (this._line < other._line) { - return true; - } - if (other._line < this._line) { - return false; - } - return this._character <= other._character; + if (character < 0) { + throw illegalArgument('character must be non-negative'); } + this._line = line; + this._character = character; + } - isAfter(other: Position): boolean { - return !this.isBeforeOrEqual(other); + isBefore(other: Position): boolean { + if (this._line < other._line) { + return true; } - - isAfterOrEqual(other: Position): boolean { - return !this.isBefore(other); + if (other._line < this._line) { + return false; } + return this._character < other._character; + } - isEqual(other: Position): boolean { - return this._line === other._line && this._character === other._character; + isBeforeOrEqual(other: Position): boolean { + if (this._line < other._line) { + return true; } - - 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; - } - } + if (other._line < this._line) { + return false; } + return this._character <= other._character; + } - 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; - } + isAfter(other: Position): boolean { + return !this.isBeforeOrEqual(other); + } - 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(); - } + isAfterOrEqual(other: Position): boolean { + return !this.isBefore(other); + } - 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; - } + isEqual(other: Position): boolean { + return this._line === other._line && this._character === other._character; + } - if (line === this.line && character === this.character) { - return this; - } - return new Position(line, character); + compareTo(other: Position): number { + if (this._line < other._line) { + return -1; } - - toJSON(): any { - return { line: this.line, character: this.character }; + if (this._line > other.line) { + return 1; } - } - - 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); + // equal line + if (this._character < other._character) { + return -1; } - - protected _start: Position; - protected _end: Position; - - get start(): Position { - return this._start; + if (this._character > other._character) { + return 1; } + // equal line and character + return 0; + } - get end(): Position { - return this._end; - } + translate(change: { lineDelta?: number; characterDelta?: number }): Position; - 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'); - } + translate(lineDelta?: number, characterDelta?: number): Position; - if (start.isBefore(end)) { - this._start = start; - this._end = end; - } else { - this._start = end; - this._end = start; - } + translate( + lineDeltaOrChange: number | { lineDelta?: number; characterDelta?: number } | undefined, + characterDelta = 0, + ): Position { + if (lineDeltaOrChange === null || characterDelta === null) { + throw illegalArgument(); } - 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; + 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; } - isEqual(other: Range): boolean { - return this._start.isEqual(other._start) && this._end.isEqual(other._end); + if (lineDelta === 0 && characterDelta === 0) { + return this; } + return new Position(this.line + lineDelta, this.character + characterDelta); + } - 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); - } + with(change: { line?: number; character?: number }): Position; - 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); - } + with(line?: number, character?: number): Position; - get isEmpty(): boolean { - return this._start.isEqual(this._end); + with( + lineOrChange: number | { line?: number; character?: number } | undefined, + character: number = this.character, + ): Position { + if (lineOrChange === null || character === null) { + throw illegalArgument(); } - get isSingleLine(): boolean { - return this._start.line === this._end.line; + 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; } - 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(); - } + if (line === this.line && character === this.character) { + return this; + } + return new Position(line, character); + } - 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; - } + toJSON(): { line: number; character: number } { + return { line: this.line, character: this.character }; + } +} - if (start.isEqual(this._start) && end.isEqual(this.end)) { - return this; - } - return new Range(start, end); +export class Range { + static isRange(thing: unknown): thing is vscode.Range { + if (thing instanceof Range) { + return true; } - - toJSON(): any { - return [this.start, this.end]; + if (!thing) { + return false; } + return Position.isPosition((thing as Range).start) && Position.isPosition((thing as Range).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' - ); - } + protected _start: Position; - private _anchor: Position; + protected _end: Position; - public get anchor(): Position { - return this._anchor; - } + get start(): Position { + return this._start; + } - private _active: Position; + get end(): Position { + return this._end; + } - public get active(): Position { - return this._active; - } + constructor(start: Position, end: Position); - 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'); - } + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); - super(anchor, active); + constructor( + startLineOrStart: number | Position, + startColumnOrEnd: number | Position, + endLine?: number, + endColumn?: number, + ) { + let start: Position | undefined; + let end: Position | undefined; - this._anchor = anchor; - this._active = active; + 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; } - get isReversed(): boolean { - return this._anchor === this._end; + if (!start || !end) { + throw new Error('Invalid arguments'); } - toJSON() { - return { - start: this.start, - end: this.end, - active: this.active, - anchor: this.anchor - }; + if (start.isBefore(end)) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; } } - export enum EndOfLine { - LF = 1, - CRLF = 2 - } - - export class TextEdit { - static isTextEdit(thing: any): thing is TextEdit { - if (thing instanceof TextEdit) { - return true; + contains(positionOrRange: Position | Range): boolean { + if (positionOrRange instanceof Range) { + return this.contains(positionOrRange._start) && this.contains(positionOrRange._end); + } + if (positionOrRange instanceof Position) { + if (positionOrRange.isBefore(this._start)) { + return false; } - if (!thing) { + if (this._end.isBefore(positionOrRange)) { return false; } - return Range.isRange(<TextEdit>thing) && typeof (<TextEdit>thing).newText === 'string'; + return true; } + return false; + } - static replace(range: Range, newText: string): TextEdit { - return new TextEdit(range, newText); - } + isEqual(other: Range): boolean { + return this._start.isEqual(other._start) && this._end.isEqual(other._end); + } - static insert(position: Position, newText: string): TextEdit { - return TextEdit.replace(new Range(position, position), newText); + intersection(other: Range): Range | undefined { + const start = Position.Max(other.start, this._start); + const end = Position.Min(other.end, this._end); + if (start.isAfter(end)) { + // this happens when there is no overlap: + // |-----| + // |----| + return undefined; } + return new Range(start, end); + } - static delete(range: Range): TextEdit { - return TextEdit.replace(range, ''); + union(other: Range): Range { + if (this.contains(other)) { + return this; } - - static setEndOfLine(eol: EndOfLine): TextEdit { - // @ts-ignore - let ret = new TextEdit(undefined, undefined); - ret.newEol = eol; - return ret; + if (other.contains(this)) { + return other; } - // @ts-ignore - protected _range: Range; - // @ts-ignore - protected _newText: string; - // @ts-ignore - protected _newEol: EndOfLine; + const start = Position.Min(other.start, this._start); + const end = Position.Max(other.end, this.end); + return new Range(start, end); + } - get range(): Range { - return this._range; - } + get isEmpty(): boolean { + return this._start.isEqual(this._end); + } - set range(value: Range) { - if (value && !Range.isRange(value)) { - throw illegalArgument('range'); - } - this._range = value; - } + get isSingleLine(): boolean { + return this._start.line === this._end.line; + } - get newText(): string { - return this._newText || ''; - } + with(change: { start?: Position; end?: Position }): Range; - set newText(value: string) { - if (value && typeof value !== 'string') { - throw illegalArgument('newText'); - } - this._newText = value; - } + with(start?: Position, end?: Position): Range; - get newEol(): EndOfLine { - return this._newEol; + with(startOrChange: Position | { start?: Position; end?: Position } | undefined, end: Position = this.end): Range { + if (startOrChange === null || end === null) { + throw illegalArgument(); } - set newEol(value: EndOfLine) { - if (value && typeof value !== 'number') { - throw illegalArgument('newEol'); - } - this._newEol = value; + 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; } - constructor(range: Range, newText: string) { - this.range = range; - this.newText = newText; + if (start.isEqual(this._start) && end.isEqual(this.end)) { + return this; } + return new Range(start, end); + } - toJSON(): any { - return { - range: this.range, - newText: this.newText, - newEol: this._newEol - }; - } + toJSON(): [Position, Position] { + return [this.start, this.end]; } +} - export class WorkspaceEdit implements vscode.WorkspaceEdit { - private _seqPool: number = 0; +export class Selection extends Range { + static isSelection(thing: unknown): 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 _resourceEdits: { seq: number; from: vscUri.URI; to: vscUri.URI }[] = []; - private _textEdits = new Map<string, { seq: number; uri: vscUri.URI; edits: TextEdit[] }>(); + private _anchor: Position; - // createResource(uri: vscode.Uri): void { - // this.renameResource(undefined, uri); - // } + public get anchor(): Position { + return this._anchor; + } - // deleteResource(uri: vscode.Uri): void { - // this.renameResource(uri, undefined); - // } + private _active: Position; - // renameResource(from: vscode.Uri, to: vscode.Uri): void { - // this._resourceEdits.push({ seq: this._seqPool++, from, to }); - // } + public get active(): Position { + return this._active; + } - // resourceEdits(): [vscode.Uri, vscode.Uri][] { - // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); - // } + constructor(anchor: Position, active: Position); - 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.'); - } + constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number); - 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); - } + constructor( + anchorLineOrAnchor: number | Position, + anchorColumnOrActive: number | Position, + activeLine?: number, + activeColumn?: number, + ) { + let anchor: Position | undefined; + let active: Position | undefined; - insert(resource: vscUri.URI, position: Position, newText: string): void { - this.replace(resource, new Range(position, position), newText); + 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; } - delete(resource: vscUri.URI, range: Range): void { - this.replace(resource, range, ''); + if (!anchor || !active) { + throw new Error('Invalid arguments'); } - has(uri: vscUri.URI): boolean { - return this._textEdits.has(uri.toString()); - } + super(anchor, active); - 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); - } - } + this._anchor = anchor; + this._active = active; + } - 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; - } + get isReversed(): boolean { + return this._anchor === this._end; + } - entries(): [vscUri.URI, TextEdit[]][] { - const res: [vscUri.URI, TextEdit[]][] = []; - this._textEdits.forEach((value) => res.push([value.uri, value.edits])); - return res.slice(); - } + toJSON(): [Position, Position] { + return ({ + start: this.start, + end: this.end, + active: this.active, + anchor: this.anchor, + } as unknown) as [Position, Position]; + } +} - 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; - } +export enum EndOfLine { + LF = 1, + CRLF = 2, +} - get size(): number { - return this._textEdits.size + this._resourceEdits.length; +export class TextEdit { + static isTextEdit(thing: unknown): thing is TextEdit { + if (thing instanceof TextEdit) { + return true; } - - toJSON(): any { - return this.entries(); + if (!thing) { + return false; } + return Range.isRange(<TextEdit>thing) && typeof (<TextEdit>thing).newText === 'string'; } - 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, '\\$&'); - } + static replace(range: Range, newText: string): TextEdit { + return new TextEdit(range, newText); + } - private _tabstop: number = 1; + static insert(position: Position, newText: string): TextEdit { + return TextEdit.replace(new Range(position, position), newText); + } - value: string; + static delete(range: Range): TextEdit { + return TextEdit.replace(range, ''); + } - constructor(value?: string) { - this.value = value || ''; - } + static setEndOfLine(eol: EndOfLine): TextEdit { + const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); + ret.newEol = eol; + return ret; + } - appendText(string: string): SnippetString { - this.value += SnippetString._escape(string); - return this; - } + _range: Range = new Range(new Position(0, 0), new Position(0, 0)); - appendTabstop(number: number = this._tabstop++): SnippetString { - this.value += '$'; - this.value += number; - return this; - } + newText = ''; - 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); - } + _newEol: EndOfLine = EndOfLine.LF; - this.value += '${'; - this.value += number; - this.value += ':'; - this.value += value; - this.value += '}'; + get range(): Range { + return this._range; + } - return this; + set range(value: Range) { + if (value && !Range.isRange(value)) { + throw illegalArgument('range'); } + this._range = value; + } - 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 += '|}'; + get newEol(): EndOfLine { + return this._newEol; + } - return this; + set newEol(value: EndOfLine) { + if (value && typeof value !== 'number') { + throw illegalArgument('newEol'); } + this._newEol = value; + } - 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, '\\$&'); - } + constructor(range: Range, newText: string) { + this.range = range; + this.newText = newText; + } +} - this.value += '${'; - this.value += name; - if (defaultValue) { - this.value += ':'; - this.value += defaultValue; - } - this.value += '}'; +export class WorkspaceEdit implements vscode.WorkspaceEdit { + // eslint-disable-next-line class-methods-use-this + appendNotebookCellOutput( + _uri: vscode.Uri, + _index: number, + _outputs: vscode.NotebookCellOutput[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } - return this; - } + // eslint-disable-next-line class-methods-use-this + replaceNotebookCellOutputItems( + _uri: vscode.Uri, + _index: number, + _outputId: string, + _items: vscode.NotebookCellOutputItem[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. } - export enum DiagnosticTag { - Unnecessary = 1 + // eslint-disable-next-line class-methods-use-this + appendNotebookCellOutputItems( + _uri: vscode.Uri, + _index: number, + _outputId: string, + _items: vscode.NotebookCellOutputItem[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. } - export enum DiagnosticSeverity { - Hint = 3, - Information = 2, - Warning = 1, - Error = 0 + // eslint-disable-next-line class-methods-use-this + replaceNotebookCells( + _uri: vscode.Uri, + _start: number, + _end: number, + _cells: vscode.NotebookCellData[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. } - 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); - } + // eslint-disable-next-line class-methods-use-this + replaceNotebookCellOutput( + _uri: vscode.Uri, + _index: number, + _outputs: vscode.NotebookCellOutput[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } - uri: vscUri.URI; - // @ts-ignore - range: Range; + private _seqPool = 0; - constructor(uri: vscUri.URI, rangeOrPosition: Range | Position) { - this.uri = uri; + private _resourceEdits: { seq: number; from: vscUri.URI; to: vscUri.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'); - } - } + private _textEdits = new Map<string, { seq: number; uri: vscUri.URI; edits: TextEdit[] }>(); - toJSON(): any { - return { - uri: this.uri, - range: this.range - }; - } - } + // createResource(uri: vscode.Uri): void { + // this.renameResource(undefined, uri); + // } - 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 - }; - } - } + // deleteResource(uri: vscode.Uri): void { + // this.renameResource(uri, undefined); + // } - export class Hover { - public contents: vscode.MarkdownString[] | vscode.MarkedString[]; - public range: Range; + // renameResource(from: vscode.Uri, to: vscode.Uri): void { + // this._resourceEdits.push({ seq: this._seqPool+= 1, from, to }); + // } - 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; - } + // resourceEdits(): [vscode.Uri, vscode.Uri][] { + // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); + // } + + // eslint-disable-next-line class-methods-use-this + createFile(_uri: vscode.Uri, _options?: { overwrite?: boolean; ignoreIfExists?: boolean }): void { + throw new Error('Method not implemented.'); } - export enum DocumentHighlightKind { - Text = 0, - Read = 1, - Write = 2 + // eslint-disable-next-line class-methods-use-this + deleteFile(_uri: vscode.Uri, _options?: { recursive?: boolean; ignoreIfNotExists?: boolean }): void { + throw new Error('Method not implemented.'); } - export class DocumentHighlight { - range: Range; - kind: DocumentHighlightKind; + // eslint-disable-next-line class-methods-use-this + renameFile( + _oldUri: vscode.Uri, + _newUri: vscode.Uri, + _options?: { overwrite?: boolean; ignoreIfExists?: boolean }, + ): void { + throw new Error('Method not implemented.'); + } - constructor(range: Range, kind: DocumentHighlightKind = DocumentHighlightKind.Text) { - this.range = range; - this.kind = kind; + replace(uri: vscUri.URI, range: Range, newText: string): void { + const edit = new TextEdit(range, newText); + let array = this.get(uri); + if (array) { + array.push(edit); + } else { + array = [edit]; } + this.set(uri, array); + } - toJSON(): any { - return { - range: this.range, - kind: DocumentHighlightKind[this.kind] - }; - } + insert(resource: vscUri.URI, position: Position, newText: string): void { + this.replace(resource, new Range(position, position), newText); } - 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; + delete(resource: vscUri.URI, range: Range): void { + this.replace(resource, range, ''); + } - if (typeof rangeOrContainer === 'string') { - this.containerName = rangeOrContainer; - } + has(uri: vscUri.URI): boolean { + return this._textEdits.has(uri.toString()); + } - if (locationOrUri instanceof Location) { - this.location = locationOrUri; - } else if (rangeOrContainer instanceof Range) { - // @ts-ignore - this.location = new Location(locationOrUri, rangeOrContainer); - } + set(uri: vscUri.URI, edits: readonly unknown[]): void { + let data = this._textEdits.get(uri.toString()); + if (!data) { + data = { seq: this._seqPool += 1, uri, edits: [] }; + this._textEdits.set(uri.toString(), data); } - - toJSON(): any { - return { - name: this.name, - kind: SymbolKind[this.kind], - location: this.location, - containerName: this.containerName - }; + if (!edits) { + data.edits = []; + } else { + data.edits = edits.slice(0) as TextEdit[]; } } - 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; + get(uri: vscUri.URI): TextEdit[] { + if (!this._textEdits.has(uri.toString())) { + return []; } + const { edits } = this._textEdits.get(uri.toString()) || {}; + return edits ? edits.slice() : []; } - export enum CodeActionTrigger { - Automatic = 1, - Manual = 2 + entries(): [vscUri.URI, TextEdit[]][] { + const res: [vscUri.URI, TextEdit[]][] = []; + this._textEdits.forEach((value) => res.push([value.uri, value.edits])); + return res.slice(); } - export class CodeAction { - title: string; - - command?: vscode.Command; - - edit?: WorkspaceEdit; + 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; + } - dianostics?: Diagnostic[]; + get size(): number { + return this._textEdits.size + this._resourceEdits.length; + } - kind?: CodeActionKind; + toJSON(): [vscUri.URI, TextEdit[]][] { + return this.entries(); + } +} - constructor(title: string, kind?: CodeActionKind) { - this.title = title; - this.kind = kind; +export class SnippetString { + static isSnippetString(thing: unknown): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; + } + if (!thing) { + return false; } + return typeof (<SnippetString>thing).value === 'string'; } - export class CodeActionKind { - private static readonly sep = '.'; + private static _escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } - 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'); + private _tabstop = 1; - constructor(public readonly value: string) {} + value: string; - public append(parts: string): CodeActionKind { - return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); - } + constructor(value?: string) { + this.value = value || ''; + } - public contains(other: CodeActionKind): boolean { - return ( - this.value === other.value || vscMockStrings.startsWith(other.value, this.value + CodeActionKind.sep) - ); - } + appendText(string: string): SnippetString { + this.value += SnippetString._escape(string); + return this; } - export class CodeLens { - range: Range; + appendTabstop(number: number = (this._tabstop += 1)): SnippetString { + this.value += '$'; + this.value += number; + return this; + } - command: vscode.Command; - - constructor(range: Range, command?: vscode.Command) { - this.range = range; - // @ts-ignore - this.command = command; + appendPlaceholder( + value: string | ((snippet: SnippetString) => void), + number: number = (this._tabstop += 1), + ): 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); } - get isResolved(): boolean { - return !!this.command; - } + this.value += '${'; + this.value += number; + this.value += ':'; + this.value += value; + this.value += '}'; + + return this; } - export class MarkdownString { - value: string; - isTrusted?: boolean; + appendChoice(values: string[], number: number = (this._tabstop += 1)): SnippetString { + const value = SnippetString._escape(values.toString()); + + this.value += '${'; + this.value += number; + this.value += '|'; + this.value += value; + this.value += '|}'; - constructor(value?: string) { - this.value = value || ''; + return this; + } + + appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => void)): 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, '\\$&'); // CodeQL [SM02383] don't escape backslashes here (by design) } - appendText(value: string): MarkdownString { - // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash - this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); - return this; + this.value += '${'; + this.value += name; + if (defaultValue) { + this.value += ':'; + this.value += defaultValue; } + this.value += '}'; - appendMarkdown(value: string): MarkdownString { - this.value += value; - return this; + return this; + } +} + +export enum DiagnosticTag { + Unnecessary = 1, +} + +export enum DiagnosticSeverity { + Hint = 3, + Information = 2, + Warning = 1, + Error = 0, +} + +export class Location { + static isLocation(thing: unknown): 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); + } - appendCodeblock(code: string, language: string = ''): MarkdownString { - this.value += '\n```'; - this.value += language; - this.value += '\n'; - this.value += code; - this.value += '\n```\n'; - return this; + uri: vscUri.URI; + + range: Range = new Range(new Position(0, 0), new Position(0, 0)); + + 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'); } } - export class ParameterInformation { - label: string; - documentation?: string | MarkdownString; + toJSON(): { uri: vscUri.URI; range: Range } { + return { + uri: this.uri, + range: this.range, + }; + } +} - constructor(label: string, documentation?: string | MarkdownString) { - this.label = label; - this.documentation = documentation; +export class DiagnosticRelatedInformation { + static is(thing: unknown): 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) + ); } - export class SignatureInformation { - label: string; - documentation?: string | MarkdownString; - parameters: ParameterInformation[]; + location: Location; - constructor(label: string, documentation?: string | MarkdownString) { - this.label = label; - this.documentation = documentation; - this.parameters = []; + message: string; + + constructor(location: Location, message: string) { + this.location = location; + this.message = message; + } +} + +export class Diagnostic { + range: Range; + + message: string; + + source = ''; + + code: string | number = ''; + + severity: DiagnosticSeverity; + + relatedInformation: DiagnosticRelatedInformation[] = []; + + customTags?: DiagnosticTag[]; + + constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { + this.range = range; + this.message = message; + this.severity = severity; + } + + toJSON(): { severity: DiagnosticSeverity; message: string; range: Range; source: string; code: string | number } { + return { + severity: (DiagnosticSeverity[this.severity] as unknown) as DiagnosticSeverity, + message: this.message, + range: this.range, + source: this.source, + code: this.code, + }; + } +} + +export class Hover { + public contents: vscode.MarkdownString[]; + + public range: Range; + + constructor(contents: vscode.MarkdownString | vscode.MarkdownString[], range?: Range) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); } + if (Array.isArray(contents)) { + this.contents = <vscode.MarkdownString[]>contents; + } else if (vscMockHtmlContent.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; + } + + this.range = range || new Range(new Position(0, 0), new Position(0, 0)); } +} + +export enum DocumentHighlightKind { + Text = 0, + Read = 1, + Write = 2, +} + +export class DocumentHighlight { + range: Range; - export class SignatureHelp { - signatures: SignatureInformation[]; - // @ts-ignore - activeSignature: number; - // @ts-ignore - activeParameter: number; + kind: DocumentHighlightKind; + + constructor(range: Range, kind: DocumentHighlightKind = DocumentHighlightKind.Text) { + this.range = range; + this.kind = kind; + } + + toJSON(): { range: Range; kind: DocumentHighlightKind } { + return { + range: this.range, + kind: (DocumentHighlightKind[this.kind] as unknown) as DocumentHighlightKind, + }; + } +} + +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, +} - constructor() { - this.signatures = []; +export class SymbolInformation { + name: string; + + location: Location = new Location( + vscUri.URI.parse('testLocation'), + new Range(new Position(0, 0), new Position(0, 0)), + ); + + 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; + this.containerName = containerName || ''; + + if (typeof rangeOrContainer === 'string') { + this.containerName = rangeOrContainer; + } + + if (locationOrUri instanceof Location) { + this.location = locationOrUri; + } else if (rangeOrContainer instanceof Range) { + this.location = new Location(locationOrUri as vscUri.URI, rangeOrContainer); } } - export enum CompletionTriggerKind { - Invoke = 0, - TriggerCharacter = 1, - TriggerForIncompleteCompletions = 2 + toJSON(): { name: string; kind: SymbolKind; location: Location; containerName: string } { + return { + name: this.name, + kind: (SymbolKind[this.kind] as unknown) as SymbolKind, + 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 | undefined; + + constructor(range: Range, command?: vscode.Command) { + this.range = range; + 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 = ''): 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; - export interface CompletionContext { - triggerKind: CompletionTriggerKind; - triggerCharacter: string; + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; } +} + +export class SignatureInformation { + label: string; + + documentation?: string | MarkdownString; + + parameters: ParameterInformation[]; - 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 + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + this.parameters = []; } +} + +export class SignatureHelp { + signatures: SignatureInformation[]; + + activeSignature: number; + + activeParameter: number; - export enum CompletionItemTag { - Deprecated = 1 + constructor() { + this.signatures = []; + this.activeSignature = -1; + this.activeParameter = -1; } +} + +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 { + 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; - export interface CompletionItemLabel { - name: string; - signature?: string; - qualifier?: string; - type?: string; + commitCharacters?: string[]; + + textEdit?: TextEdit; + + additionalTextEdits?: TextEdit[]; + + command?: vscode.Command; + + constructor(label: string, kind?: CompletionItemKind) { + this.label = label; + this.kind = kind; } - export class CompletionItem { - // @ts-ignore + toJSON(): { 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 - }; - } + } { + return { + label: this.label, + label2: this.label2, + kind: this.kind && ((CompletionItemKind[this.kind] as unknown) as CompletionItemKind), + 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; +export class CompletionList { + isIncomplete?: boolean; - items: vscode.CompletionItem[]; + items: vscode.CompletionItem[]; - constructor(items: vscode.CompletionItem[] = [], isIncomplete: boolean = false) { - this.items = items; - this.isIncomplete = isIncomplete; - } + constructor(items: vscode.CompletionItem[] = [], isIncomplete = 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 class CallHierarchyItem { + name: string; - export enum StatusBarAlignment { - Left = 1, - Right = 2 - } + kind: SymbolKind; - export enum TextEditorLineNumbersStyle { - Off = 0, - On = 1, - Relative = 2 - } + tags?: ReadonlyArray<vscode.SymbolTag>; - export enum TextDocumentSaveReason { - Manual = 1, - AfterDelay = 2, - FocusOut = 3 - } + detail?: string; - export enum TextEditorRevealType { - Default = 0, - InCenter = 1, - InCenterIfOutsideViewport = 2, - AtTop = 3 - } + uri: vscode.Uri; + + range: vscode.Range; + + selectionRange: vscode.Range; - export enum TextEditorSelectionChangeKind { - Keyboard = 1, - Mouse = 2, - Command = 3 + constructor( + kind: vscode.SymbolKind, + name: string, + detail: string, + uri: vscode.Uri, + range: vscode.Range, + selectionRange: vscode.Range, + ) { + this.kind = kind; + this.name = name; + this.detail = detail; + this.uri = uri; + this.range = range; + this.selectionRange = selectionRange; } +} + +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, +} + +// eslint-disable-next-line import/export +export enum TextEditorSelectionChangeKind { + Keyboard = 1, + Mouse = 2, + Command = 3, +} + +/** + * These values match very carefully the values of `TrackedRangeStickiness` + */ +export enum DecorationRangeBehavior { /** - * These values match very carefully the values of `TrackedRangeStickiness` + * TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges */ - 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; + OpenOpen = 0, + /** + * TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + */ + ClosedClosed = 1, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + */ + OpenClosed = 2, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingAfter + */ + ClosedOpen = 3, +} + +// eslint-disable-next-line import/export, @typescript-eslint/no-namespace +export namespace TextEditorSelectionChangeKind { + export function fromValue(s: string): TextEditorSelectionChangeKind | undefined { + switch (s) { + case 'keyboard': + return TextEditorSelectionChangeKind.Keyboard; + case 'mouse': + return TextEditorSelectionChangeKind.Mouse; + case 'api': + return TextEditorSelectionChangeKind.Command; + default: + return undefined; } } +} - export class DocumentLink { - range: Range; +export class DocumentLink { + range: Range; - target: vscUri.URI; + 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; + 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; +export class Color { + readonly red: number; - constructor(red: number, green: number, blue: number, alpha: number) { - this.red = red; - this.green = green; - this.blue = blue; - this.alpha = alpha; - } + 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 type IColorFormat = string | { opaque: string; transparent: string }; - export class ColorInformation { - range: Range; +export class ColorInformation { + range: Range; - color: Color; + 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; + 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[]; +export class ColorPresentation { + label: string; - constructor(label: string) { - if (!label || typeof label !== 'string') { - throw illegalArgument('label'); - } - this.label = label; + 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 ColorFormat { + RGB = 0, + HEX = 1, + HSL = 2, +} - export enum SourceControlInputBoxValidationType { - Error = 0, - Warning = 1, - Information = 2 - } +export enum SourceControlInputBoxValidationType { + Error = 0, + Warning = 1, + Information = 2, +} - export enum TaskRevealKind { - Always = 1, +export enum TaskRevealKind { + Always = 1, - Silent = 2, + Silent = 2, - Never = 3 - } + Never = 3, +} - export enum TaskPanelKind { - Shared = 1, +export enum TaskPanelKind { + Shared = 1, - Dedicated = 2, + Dedicated = 2, - New = 3 - } + New = 3, +} - export class TaskGroup implements vscode.TaskGroup { - private _id: string; +export class TaskGroup implements vscode.TaskGroup { + private _id: string; - public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); + public isDefault = undefined; - public static Build: TaskGroup = new TaskGroup('build', 'Build'); + public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); - public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); + public static Build: TaskGroup = new TaskGroup('build', 'Build'); - public static Test: TaskGroup = new TaskGroup('test', 'Test'); + public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); - 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; - } - } + public static Test: TaskGroup = new TaskGroup('test', 'Test'); - constructor(id: string, _label: string) { - if (typeof id !== 'string') { - throw illegalArgument('name'); - } - if (typeof _label !== 'string') { - throw illegalArgument('name'); - } - this._id = id; + public static from(value: string): TaskGroup | undefined { + 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; } + } - get id(): string { - return this._id; + 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; +export class ProcessExecution implements vscode.ProcessExecution { + private _process: string; - 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 = []; - } - } + private _args: string[] | undefined; - get process(): string { - return this._process; - } + private _options: vscode.ProcessExecutionOptions | undefined; - set process(value: string) { - if (typeof value !== 'string') { - throw illegalArgument('process'); - } - this._process = value; - } + constructor(process: string, options?: vscode.ProcessExecutionOptions); - get args(): string[] { - return this._args; - } + constructor(process: string, args: string[], options?: vscode.ProcessExecutionOptions); - set args(value: string[]) { - if (!Array.isArray(value)) { - value = []; + constructor( + process: string, + varg1?: string[] | vscode.ProcessExecutionOptions, + varg2?: vscode.ProcessExecutionOptions, + ) { + if (typeof process !== 'string') { + throw illegalArgument('process'); + } + this._process = process; + if (varg1) { + if (Array.isArray(varg1)) { + this._args = varg1; + this._options = varg2; + } else { + this._options = varg1; } - this._args = value; } - get options(): vscode.ProcessExecutionOptions { - return this._options; + if (this._args === undefined) { + this._args = []; } + } - set options(value: vscode.ProcessExecutionOptions) { - this._options = value; + 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 || []; + } - 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'); + set args(value: string[]) { + if (!Array.isArray(value)) { + value = []; } + this._args = value; } - export class ShellExecution implements vscode.ShellExecution { - // @ts-ignore + get options(): vscode.ProcessExecutionOptions { + return this._options || {}; + } - private _commandLine: string; - // @ts-ignore - private _command: string | vscode.ShellQuotedString; - // @ts-ignore - private _args: (string | vscode.ShellQuotedString)[]; - private _options: vscode.ShellExecutionOptions; + set options(value: vscode.ProcessExecutionOptions) { + this._options = value; + } - 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; - } - } + // eslint-disable-next-line class-methods-use-this + 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'); + } +} - get commandLine(): string { - return this._commandLine; - } +export class ShellExecution implements vscode.ShellExecution { + private _commandLine = ''; - set commandLine(value: string) { - if (typeof value !== 'string') { - throw illegalArgument('commandLine'); - } - this._commandLine = value; - } + private _command: string | vscode.ShellQuotedString = ''; - get command(): string | vscode.ShellQuotedString { - return this._command; - } + private _args: (string | vscode.ShellQuotedString)[] = []; + + private _options: vscode.ShellExecutionOptions | undefined; - set command(value: string | vscode.ShellQuotedString) { - if (typeof value !== 'string' && typeof value.value !== 'string') { + 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 = value; + this._command = arg0; + this._args = arg1 as (string | vscode.ShellQuotedString)[]; + this._options = arg2; + } else { + if (typeof arg0 !== 'string') { + throw illegalArgument('commandLine'); + } + this._commandLine = arg0; + this._options = arg1; } + } - get args(): (string | vscode.ShellQuotedString)[] { - return this._args; - } + get commandLine(): string { + return this._commandLine; + } - set args(value: (string | vscode.ShellQuotedString)[]) { - this._args = value || []; + set commandLine(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('commandLine'); } + this._commandLine = value; + } - get options(): vscode.ShellExecutionOptions { - return this._options; - } + get command(): string | vscode.ShellQuotedString { + return this._command; + } - set options(value: vscode.ShellExecutionOptions) { - this._options = value; + set command(value: string | vscode.ShellQuotedString) { + if (typeof value !== 'string' && typeof value.value !== 'string') { + throw illegalArgument('command'); } + this._command = 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'); - } + get args(): (string | vscode.ShellQuotedString)[] { + return this._args; } - export enum ShellQuoting { - Escape = 1, - Strong = 2, - Weak = 3 + set args(value: (string | vscode.ShellQuotedString)[]) { + this._args = value || []; } - export enum TaskScope { - Global = 1, - Workspace = 2 + get options(): vscode.ShellExecutionOptions { + return this._options || {}; } - export class Task implements vscode.Task { - private static ProcessType: string = 'process'; - private static ShellType: string = 'shell'; - private static EmptyType: string = '$empty'; + set options(value: vscode.ShellExecutionOptions) { + this._options = value; + } - private __id: string | undefined; + // eslint-disable-next-line class-methods-use-this + 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'); + } +} - 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; +export enum ShellQuoting { + Escape = 1, + Strong = 2, + Weak = 3, +} - 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); - } +export enum TaskScope { + Global = 1, + Workspace = 2, +} - get _id(): string | undefined { - return this.__id; - } +export class Task implements vscode.Task { + private static ProcessType = 'process'; - set _id(value: string | undefined) { - this.__id = value; - } + private static ShellType = 'shell'; - private clear(): void { - if (this.__id === undefined) { - return; - } - this.__id = undefined; - this._scope = undefined; - this.computeDefinitionBasedOnExecution(); - } + private static EmptyType = '$empty'; - 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() - }; - } - } + private __id: string | undefined; - get definition(): vscode.TaskDefinition { - return this._definition; - } + private _definition!: vscode.TaskDefinition; - set definition(value: vscode.TaskDefinition) { - if (value === undefined || value === null) { - throw illegalArgument("Kind can't be undefined or null"); - } - this.clear(); - this._definition = value; - } + private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined; - get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined { - return this._scope; - } + private _name!: string; - set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { - this.clear(); - this._scope = value; - } + private _execution: ProcessExecution | ShellExecution | undefined; - get name(): string { - return this._name; - } + private _problemMatchers: string[]; - set name(value: string) { - if (typeof value !== 'string') { - throw illegalArgument('name'); - } - this.clear(); - this._name = value; - } + private _hasDefinedMatchers: boolean; - get execution(): ProcessExecution | ShellExecution | undefined { - return this._execution; - } + private _isBackground: boolean; - 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(); - } - } + private _source!: string; - get problemMatchers(): string[] { - return this._problemMatchers; - } + private _group: TaskGroup | undefined; - 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; - } - } + private _presentationOptions: vscode.TaskPresentationOptions; - get hasDefinedMatchers(): boolean { - return this._hasDefinedMatchers; - } + private _runOptions: vscode.RunOptions; - get isBackground(): boolean { - return this._isBackground; - } + constructor( + definition: vscode.TaskDefinition, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[], + ); - set isBackground(value: boolean) { - if (value !== true && value !== false) { - value = false; - } - this.clear(); - this._isBackground = value; - } + constructor( + definition: vscode.TaskDefinition, + scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[], + ); - get source(): string { - return this._source; + constructor( + definition: vscode.TaskDefinition, + arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, + arg3: string, + arg4?: string | ProcessExecution | ShellExecution, + arg5?: ProcessExecution | ShellExecution | string | string[], + arg6?: string | string[], + ) { + this.definition = definition; + let problemMatchers: string | string[]; + if (typeof arg2 === 'string') { + this.name = arg2; + this.source = arg3; + this.execution = arg4 as ProcessExecution | ShellExecution; + problemMatchers = arg5 as string | string[]; + } else { + this.target = arg2; + this.name = arg3; + this.source = arg4 as string; + this.execution = arg5 as ProcessExecution | ShellExecution; + problemMatchers = arg6 as string | string[]; } - - 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; + 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 group(): TaskGroup | undefined { - return this._group; + 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(); + } - set group(value: TaskGroup | undefined) { - if (value === null) { - value = undefined; - } - this.clear(); - this._group = value; + 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 presentationOptions(): vscode.TaskPresentationOptions { - return this._presentationOptions; + 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; + } - set presentationOptions(value: vscode.TaskPresentationOptions) { - if (value === null || value === undefined) { - value = Object.create(null); - } - this.clear(); - this._presentationOptions = 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; + } - get runOptions(): vscode.RunOptions { - return this._runOptions; + set execution(value: ProcessExecution | ShellExecution | undefined) { + if (value === null) { + value = undefined; } + this.clear(); + this._execution = value; + const { type } = this._definition; + if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type) { + this.computeDefinitionBasedOnExecution(); + } + } - set runOptions(value: vscode.RunOptions) { - if (value === null || value === undefined) { - value = Object.create(null); - } + get problemMatchers(): string[] { + return this._problemMatchers; + } + + set problemMatchers(value: string[]) { + if (!Array.isArray(value)) { + this.clear(); + this._problemMatchers = []; + this._hasDefinedMatchers = false; + } else { this.clear(); - this._runOptions = value; + this._problemMatchers = value; + this._hasDefinedMatchers = true; } } - export enum ProgressLocation { - SourceControl = 1, - Window = 10, - Notification = 15 + get hasDefinedMatchers(): boolean { + return this._hasDefinedMatchers; } - 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; + get isBackground(): boolean { + return this._isBackground; + } - 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; - } + set isBackground(value: boolean) { + if (value !== true && value !== false) { + value = false; } + this.clear(); + this._isBackground = value; + } + + get source(): string { + return this._source; } - export enum TreeItemCollapsibleState { - None = 0, - Collapsed = 1, - Expanded = 2 + 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; } - export class ThemeIcon { - static readonly File = new ThemeIcon('file'); + get group(): TaskGroup | undefined { + return this._group; + } - static readonly Folder = new ThemeIcon('folder'); + set group(value: TaskGroup | undefined) { + if (value === null) { + value = undefined; + } + this.clear(); + this._group = value; + } - readonly id: string; + get presentationOptions(): vscode.TaskPresentationOptions { + return this._presentationOptions; + } - private constructor(id: string) { - this.id = id; + 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; } - export class ThemeColor { - id: string; - constructor(id: string) { - this.id = id; + set runOptions(value: vscode.RunOptions) { + if (value === null || value === undefined) { + value = Object.create(null); } + this.clear(); + this._runOptions = value; } +} - export enum ConfigurationTarget { - Global = 1, +export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15, +} - Workspace = 2, +export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2, +} - WorkspaceFolder = 3 - } +/** + * Represents an icon in the UI. This is either an uri, separate uris for the light- and dark-themes, + * or a {@link ThemeIcon theme icon}. + */ +export type IconPath = + | vscUri.URI + | { + /** + * The icon path for the light theme. + */ + light: vscUri.URI; + /** + * The icon path for the dark theme. + */ + dark: vscUri.URI; + } + | ThemeIcon; - export class RelativePattern implements IRelativePattern { - base: string; - pattern: string; +export class TreeItem { + label?: string | vscode.TreeItemLabel; + id?: string; - constructor(base: vscode.WorkspaceFolder | string, pattern: string) { - if (typeof base !== 'string') { - if (!base || !vscUri.URI.isUri(base.uri)) { - throw illegalArgument('base'); - } - } + resourceUri?: vscUri.URI; - if (typeof pattern !== 'string') { - throw illegalArgument('pattern'); - } + iconPath?: string | IconPath; - this.base = typeof base === 'string' ? base : base.uri.fsPath; - this.pattern = pattern; - } + command?: vscode.Command; + + contextValue?: string; + + tooltip?: string; + + constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState); + + constructor(resourceUri: vscUri.URI, collapsibleState?: vscode.TreeItemCollapsibleState); - public pathToRelative(from: string, to: string): string { - return relative(from, to); + constructor( + arg1: string | vscUri.URI, + public collapsibleState: vscode.TreeItemCollapsibleState = TreeItemCollapsibleState.None, + ) { + if (arg1 instanceof vscUri.URI) { + this.resourceUri = arg1; + } else { + this.label = arg1; } } +} - export class Breakpoint { - readonly enabled: boolean; - readonly condition?: string; - readonly hitCondition?: string; - readonly logMessage?: string; +export class ThemeIcon { + static readonly File = new ThemeIcon('file'); - 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; - } - } + static readonly Folder = new ThemeIcon('folder'); + + readonly id: string; + + private constructor(id: string) { + this.id = id; } +} - export class SourceBreakpoint extends Breakpoint { - readonly location: Location; +export class ThemeColor { + id: string; - constructor( - location: Location, - enabled?: boolean, - condition?: string, - hitCondition?: string, - logMessage?: string - ) { - super(enabled, condition, hitCondition, logMessage); - if (location === null) { - throw illegalArgument('location'); + constructor(id: string) { + this.id = id; + } +} + +export enum ConfigurationTarget { + Global = 1, + + Workspace = 2, + + WorkspaceFolder = 3, +} + +export class RelativePattern implements IRelativePattern { + baseUri: vscode.Uri; + + 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'); } - this.location = location; } + + if (typeof pattern !== 'string') { + throw illegalArgument('pattern'); + } + + this.baseUri = typeof base === 'string' ? vscUri.URI.parse(base) : base.uri; + this.base = typeof base === 'string' ? base : base.uri.fsPath; + this.pattern = pattern; } - export class FunctionBreakpoint extends Breakpoint { - readonly functionName: string; + // eslint-disable-next-line class-methods-use-this + public pathToRelative(from: string, to: string): string { + return relative(from, to); + } +} - 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 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 DebugAdapterExecutable { - readonly command: string; - readonly args: string[]; +export class SourceBreakpoint extends Breakpoint { + readonly location: Location; - constructor(command: string, args?: string[]) { - this.command = command; - // @ts-ignore - this.args = args; + 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 DebugAdapterServer { - readonly port: number; - readonly host?: string; +export class FunctionBreakpoint extends Breakpoint { + readonly functionName: string; - constructor(port: number, host?: string) { - this.port = port; - this.host = host; + 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; + this.args = args || []; } +} + +export class DebugAdapterServer { + readonly port: number; - export enum LogLevel { - Trace = 1, - Debug = 2, - Info = 3, - Warning = 4, - Error = 5, - Critical = 6, - Off = 7 + 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 +// #region file api - export enum FileChangeType { - Changed = 1, - Created = 2, - Deleted = 3 +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); } - 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); - } + static FileNotFound(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotFound', FileSystemError.FileNotFound); + } - constructor(uriOrMessage?: string | vscUri.URI, code?: string, terminator?: Function) { - super(vscUri.URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); - this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + static FileNotADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotADirectory', FileSystemError.FileNotADirectory); + } - // 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); - } + static FileIsADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryIsADirectory', FileSystemError.FileIsADirectory); + } - if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { - // nice stack traces - Error.captureStackTrace(this, terminator); - } - } - public get code(): string { - return ''; + 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?: () => void) { + super(vscUri.URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); + this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + + Object.setPrototypeOf(this, FileSystemError.prototype); + + if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { + // nice stack traces + Error.captureStackTrace(this, terminator); } } - //#endregion + // eslint-disable-next-line class-methods-use-this + public get code(): string { + return ''; + } +} - //#region folding api +// #endregion - export class FoldingRange { - start: number; +// #region folding api - end: number; +export class FoldingRange { + start: number; - kind?: FoldingRangeKind; + end: number; - constructor(start: number, end: number, kind?: FoldingRangeKind) { - this.start = start; - this.end = end; - this.kind = kind; - } + 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: vscUri.URI.file('back') }; +} + +export enum SymbolTag { + Deprecated = 1, +} - export enum FoldingRangeKind { - Comment = 1, - Imports = 2, - Region = 3 +export class TypeHierarchyItem { + name: string; + + kind: SymbolKind; + + tags?: ReadonlyArray<SymbolTag>; + + detail?: string; + + uri: vscode.Uri; + + range: Range; + + selectionRange: Range; + + constructor(kind: SymbolKind, name: string, detail: string, uri: vscode.Uri, range: Range, selectionRange: Range) { + this.name = name; + this.kind = kind; + this.detail = detail; + this.uri = uri; + this.range = range; + this.selectionRange = selectionRange; } +} - //#endregion +export declare type LSPObject = { + [key: string]: LSPAny; +}; - export enum CommentThreadCollapsibleState { - /** - * Determines an item is collapsed - */ - Collapsed = 0, - /** - * Determines an item is expanded - */ - Expanded = 1 +export declare type LSPArray = LSPAny[]; + +export declare type integer = number; +export declare type uinteger = number; +export declare type decimal = number; + +export declare type LSPAny = LSPObject | LSPArray | string | integer | uinteger | decimal | boolean | null; + +export class ProtocolTypeHierarchyItem extends TypeHierarchyItem { + data?; + + constructor( + kind: SymbolKind, + name: string, + detail: string, + uri: vscode.Uri, + range: Range, + selectionRange: Range, + data?: LSPAny, + ) { + super(kind, name, detail, uri, range, selectionRange); + this.data = data; } +} + +export class CancellationError extends Error {} + +export class LSPCancellationError extends CancellationError { + data; - export class QuickInputButtons { - static readonly Back: vscode.QuickInputButton = { iconPath: 'back' }; + constructor(data: any) { + super(); + this.data = data; } } diff --git a/src/test/mocks/vsc/htmlContent.ts b/src/test/mocks/vsc/htmlContent.ts index 61d3c7b7b995..df07c6ac3b1c 100644 --- a/src/test/mocks/vsc/htmlContent.ts +++ b/src/test/mocks/vsc/htmlContent.ts @@ -1,102 +1,101 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 'use strict'; -import { vscMockArrays } from './arrays'; -// tslint:disable:all +import * as vscMockArrays from './arrays'; -export namespace vscMockHtmlContent { - export interface IMarkdownString { - value: string; - isTrusted?: boolean; - } +export interface IMarkdownString { + value: string; + isTrusted?: boolean; +} - export class MarkdownString implements IMarkdownString { - value: string; - isTrusted?: boolean; +export class MarkdownString implements IMarkdownString { + value: string; - constructor(value: string = '') { - this.value = value; - } + isTrusted?: boolean; - appendText(value: string): MarkdownString { - // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash - this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); - return this; - } + constructor(value = '') { + 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; - } + 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; - } + 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 isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[]): boolean { + if (isMarkdownString(oneOrMany)) { + return !oneOrMany.value; + } + if (Array.isArray(oneOrMany)) { + return oneOrMany.every(isEmptyMarkdownString); } + 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 isMarkdownString(thing: unknown): thing is IMarkdownString { + if (thing instanceof MarkdownString) { + return true; } + if (thing && typeof thing === 'object') { + return ( + typeof (<IMarkdownString>thing).value === 'string' && + (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || + (<IMarkdownString>thing).isTrusted === undefined) + ); + } + 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; - } +export function markedStringsEquals( + a: IMarkdownString | IMarkdownString[], + b: IMarkdownString | IMarkdownString[], +): boolean { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + if (Array.isArray(a) && Array.isArray(b)) { + return vscMockArrays.equals(a, b, markdownStringEqual); } + if (isMarkdownString(a) && isMarkdownString(b)) { + return markdownStringEqual(a, b); + } + 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; - } +function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true; } + if (!a || !b) { + return false; + } + return a.value === b.value && a.isTrusted === b.isTrusted; +} - export function removeMarkdownEscapes(text: string): string { - if (!text) { - return text; - } - return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); +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 index 9f6fb3c2ab76..152beb64cdf4 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -1,224 +1,596 @@ +/* eslint-disable max-classes-per-file */ // 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 * as vscMockExtHostedTypes from './extHostedTypes'; +export * as vscUri from './uri'; + +const escapeCodiconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; +export function escapeCodicons(text: string): string { + return text.replace(escapeCodiconsRegex, (match, escaped) => (escaped ? match : `\\${match}`)); +} + +export class ThemeIcon { + static readonly File: ThemeIcon; + + static readonly Folder: ThemeIcon; + + constructor(public readonly id: string, public readonly color?: ThemeColor) {} +} + +export class ThemeColor { + constructor(public readonly id: string) {} +} + +export enum ExtensionKind { + /** + * Extension runs where the UI runs. + */ + UI = 1, + + /** + * Extension runs where the remote extension host runs. + */ + Workspace = 2, +} + +export enum LanguageStatusSeverity { + Information = 0, + Warning = 1, + Error = 2, +} + +export enum QuickPickItemKind { + Separator = -1, + Default = 0, +} + +export class Disposable { + static from(...disposables: { dispose(): () => void }[]): Disposable { + return new Disposable(() => { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + + disposables = []; } - } + }); } - 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(); - } + private _callOnDispose: (() => void) | undefined; - 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; - }; + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; } - 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(); + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; } } +} - export class CancellationTokenSource { - public token: CancellationToken; - constructor() { - this.token = new CancellationToken(); - } - public cancel(): void { - this.token.cancel(); +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace l10n { + export function t(message: string, ...args: unknown[]): string; + export function t(options: { + message: string; + args?: Array<string | number | boolean> | Record<string, unknown>; + comment: string | string[]; + }): string; + + export function t( + message: + | string + | { + message: string; + args?: Array<string | number | boolean> | Record<string, unknown>; + comment: string | string[]; + }, + ...args: unknown[] + ): string { + let _message = message; + let _args: unknown[] | Record<string, unknown> | undefined = args; + if (typeof message !== 'string') { + _message = message.message; + _args = message.args ?? args; } - public dispose(): void { - this.token.dispose(); + + if ((_args as Array<string>).length > 0) { + return (_message as string).replace(/{(\d+)}/g, (match, number) => + (_args as Array<string>)[number] === undefined ? match : (_args as Array<string>)[number], + ); } + return _message as string; + } + export const bundle: { [key: string]: string } | undefined = undefined; + export const uri: vscode.Uri | undefined = undefined; +} + +export class EventEmitter<T> implements vscode.EventEmitter<T> { + public event: vscode.Event<T>; + + public emitter: NodeEventEmitter; + + constructor() { + this.event = (this.add.bind(this) as unknown) as vscode.Event<T>; + 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) => void, + _thisArgs?: EventEmitter<T>, + _disposables?: Disposable[], + ): Disposable => { + const bound = _thisArgs ? listener.bind(_thisArgs) : listener; + this.emitter.addListener('evt', bound); + return { + dispose: () => { + this.emitter.removeListener('evt', bound); + }, + } as Disposable; + }; +} + +export class CancellationToken<T> extends EventEmitter<T> implements vscode.CancellationToken { + public isCancellationRequested!: boolean; + + public onCancellationRequested: vscode.Event<T>; + + constructor() { + super(); + this.onCancellationRequested = this.add.bind(this) as vscode.Event<T>; + } + + public cancel(): void { + this.isCancellationRequested = true; + this.fire(); + } +} + +export class CancellationTokenSource { + public token: CancellationToken<unknown>; + + 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 enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2, +} + +export class MarkdownString { + public value: string; + + public isTrusted?: boolean; + + public readonly supportThemeIcons?: boolean; + + constructor(value?: string, supportThemeIcons = false) { + this.value = value ?? ''; + this.supportThemeIcons = supportThemeIcons; } - 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; + public static isMarkdownString(thing?: string | MarkdownString | unknown): thing is vscode.MarkdownString { + if (thing instanceof MarkdownString) { + return true; } + return ( + thing !== undefined && + typeof thing === 'object' && + thing !== null && + thing.hasOwnProperty('appendCodeblock') && + thing.hasOwnProperty('appendMarkdown') && + thing.hasOwnProperty('appendText') && + thing.hasOwnProperty('value') + ); + } + + public appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) + .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') + .replace(/\n/g, '\n\n'); + + return this; } - 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 appendMarkdown(value: string): MarkdownString { + this.value += value; + + return this; + } + + public appendCodeblock(code: string, language = ''): MarkdownString { + this.value += '\n```'; + this.value += language; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } +} + +export class Hover { + public contents: vscode.MarkdownString[] | vscode.MarkedString[]; + + public range: vscode.Range | undefined; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | vscode.MarkdownString[] | vscode.MarkedString[], + range?: vscode.Range, + ) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); } - public intersects(other: CodeActionKind): boolean { - return this._value.includes(other._value) || other._value.includes(this._value); + if (Array.isArray(contents)) { + this.contents = <vscode.MarkdownString[] | vscode.MarkedString[]>contents; + } else if (MarkdownString.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; } + this.range = range; + } +} - public contains(other: CodeActionKind): boolean { - return this._value.startsWith(other._value); - } +export class CodeActionKind { + public static readonly Empty: CodeActionKind = new CodeActionKind('empty'); - public get value(): string { - return this._value; - } + 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 RefactorMove: CodeActionKind = new CodeActionKind('refactor.move'); + + 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'); + + public static readonly Notebook: CodeActionKind = new CodeActionKind('notebook'); + + private constructor(private _value: string) {} + + public append(parts: string): CodeActionKind { + return new CodeActionKind(`${this._value}.${parts}`); } - // tslint:disable-next-line: interface-name - export interface DebugAdapterExecutableOptions { - env?: { [key: string]: string }; - cwd?: string; + public intersects(other: CodeActionKind): boolean { + return this._value.includes(other._value) || other._value.includes(this._value); } - export class DebugAdapterServer { - constructor(public readonly port: number, public readonly host?: string) {} + public contains(other: CodeActionKind): boolean { + return this._value.startsWith(other._value); } - export class DebugAdapterExecutable { - constructor( - public readonly command: string, - public readonly args: string[] = [], - public readonly options?: DebugAdapterExecutableOptions - ) {} + + public get value(): string { + return this._value; } +} + +export interface DebugAdapterExecutableOptions { + env?: { [key: string]: string }; + cwd?: string; +} - export enum FileType { - Unknown = 0, - File = 1, - Directory = 2, - SymbolicLink = 64 +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, +} + +export enum UIKind { + Desktop = 1, + Web = 2, +} + +export class InlayHint { + tooltip?: string | MarkdownString | undefined; + + textEdits?: vscode.TextEdit[]; + + paddingLeft?: boolean; + + paddingRight?: boolean; + + constructor( + public position: vscode.Position, + public label: string | vscode.InlayHintLabelPart[], + public kind?: vscode.InlayHintKind, + ) {} +} + +export enum LogLevel { + /** + * No messages are logged with this level. + */ + Off = 0, + + /** + * All messages are logged with this level. + */ + Trace = 1, + + /** + * Messages with debug and higher log level are logged with this level. + */ + Debug = 2, + + /** + * Messages with info and higher log level are logged with this level. + */ + Info = 3, + + /** + * Messages with warning and higher log level are logged with this level. + */ + Warning = 4, + + /** + * Only error messages are logged with this level. + */ + Error = 5, +} + +export class TestMessage { + /** + * Human-readable message text to display. + */ + message: string | MarkdownString; + + /** + * Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown. + */ + expectedOutput?: string; + + /** + * Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown. + */ + actualOutput?: string; + + /** + * Associated file location. + */ + location?: vscode.Location; + + /** + * Creates a new TestMessage that will present as a diff in the editor. + * @param message Message to display to the user. + * @param expected Expected output. + * @param actual Actual output. + */ + static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage { + const testMessage = new TestMessage(message); + testMessage.expectedOutput = expected; + testMessage.actualOutput = actual; + return testMessage; + } + + /** + * Creates a new TestMessage instance. + * @param message The message to show to the user. + */ + constructor(message: string | MarkdownString) { + this.message = message; + } +} + +export interface TestItemCollection extends Iterable<[string, vscode.TestItem]> { + /** + * Gets the number of items in the collection. + */ + readonly size: number; + + /** + * Replaces the items stored by the collection. + * @param items Items to store. + */ + replace(items: readonly vscode.TestItem[]): void; + + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (item: vscode.TestItem, collection: TestItemCollection) => unknown, thisArg?: unknown): void; + + /** + * Adds the test item to the children. If an item with the same ID already + * exists, it'll be replaced. + * @param item Item to add. + */ + add(item: vscode.TestItem): void; + + /** + * Removes a single test item from the collection. + * @param itemId Item ID to delete. + */ + delete(itemId: string): void; + + /** + * Efficiently gets a test item by ID, if it exists, in the children. + * @param itemId Item ID to get. + * @returns The found item or undefined if it does not exist. + */ + get(itemId: string): vscode.TestItem | undefined; +} + +/** + * Represents a location inside a resource, such as a line + * inside a text file. + */ +export class Location { + /** + * The resource identifier of this location. + */ + uri: vscode.Uri; + + /** + * The document range of this location. + */ + range: vscode.Range; + + /** + * Creates a new location object. + * + * @param uri The resource identifier. + * @param rangeOrPosition The range or position. Positions will be converted to an empty range. + */ + constructor(uri: vscode.Uri, rangeOrPosition: vscode.Range) { + this.uri = uri; + this.range = rangeOrPosition; } } + +/** + * The kind of executions that {@link TestRunProfile TestRunProfiles} control. + */ +export enum TestRunProfileKind { + /** + * The `Run` test profile kind. + */ + Run = 1, + /** + * The `Debug` test profile kind. + */ + Debug = 2, + /** + * The `Coverage` test profile kind. + */ + Coverage = 3, +} diff --git a/src/test/mocks/vsc/position.ts b/src/test/mocks/vsc/position.ts index b3dff6149f3a..b05107e0be79 100644 --- a/src/test/mocks/vsc/position.ts +++ b/src/test/mocks/vsc/position.ts @@ -1,148 +1,145 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + 'use strict'; -// tslint:disable:all -export namespace vscMockPosition { +/** + * A position in the editor. This interface is suitable for serialization. + */ +export interface IPosition { /** - * A position in the editor. This interface is suitable for serialization. + * line number (starts at 1) */ - 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; - } + 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 { /** - * A position in the editor. + * line number (starts at 1) */ - 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; - } + public readonly lineNumber: number; - /** - * Test if this position equals other position - */ - public equals(other: IPosition): boolean { - return Position.equals(this, other); - } + /** + * column (the first character in a line is between column 1 and column 2) + */ + public readonly column: number; - /** - * 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; - } + constructor(lineNumber: number, column: number) { + this.lineNumber = lineNumber; + this.column = 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 this position equals other position + */ + public equals(other: IPosition): boolean { + return Position.equals(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 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 true. - */ - public isBeforeOrEqual(other: IPosition): boolean { - return Position.isBeforeOrEqual(this, other); - } + /** + * 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 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; + /** + * 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; + } - /** - * 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; - } + /** + * 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); + } - return aLineNumber - bLineNumber; + /** + * 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; } - - /** - * Clone this position. - */ - public clone(): Position { - return new Position(this.lineNumber, this.column); + if (b.lineNumber < a.lineNumber) { + return false; } + return a.column <= b.column; + } - /** - * Convert to a human-readable representation. - */ - public toString(): string { - return '(' + this.lineNumber + ',' + this.column + ')'; + /** + * A function that compares positions, useful for sorting + */ + public static compare(a: IPosition, b: IPosition): number { + const aLineNumber = a.lineNumber | 0; + const bLineNumber = b.lineNumber | 0; + + if (aLineNumber === bLineNumber) { + const aColumn = a.column | 0; + const bColumn = b.column | 0; + return aColumn - bColumn; } - // --- + return aLineNumber - bLineNumber; + } - /** - * Create a `Position` from an `IPosition`. - */ - public static lift(pos: IPosition): Position { - return new Position(pos.lineNumber, pos.column); - } + /** + * Clone this position. + */ + public clone(): Position { + return new Position(this.lineNumber, this.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'; - } + /** + * 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?: { lineNumber: unknown; column: unknown }): obj is IPosition { + return obj !== undefined && typeof obj.lineNumber === 'number' && typeof obj.column === 'number'; } } diff --git a/src/test/mocks/vsc/range.ts b/src/test/mocks/vsc/range.ts index c2667e2ce9bc..538e9ec7b9d2 100644 --- a/src/test/mocks/vsc/range.ts +++ b/src/test/mocks/vsc/range.ts @@ -1,405 +1,397 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. '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; - } +import * as vscMockPosition from './position'; + +/** + * A range in the editor. This interface is suitable for serialization. + */ +export interface IRange { /** - * 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; - } - } + * 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; +} - /** - * Test if this range is empty. - */ - public isEmpty(): boolean { - return Range.isEmpty(this); - } +/** + * 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; - /** - * Test if `range` is empty. - */ - public static isEmpty(range: IRange): boolean { - return range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn; - } + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + public readonly startColumn: number; - /** - * 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); - } + /** + * Line number on which the range ends. + */ + public readonly endLineNumber: number; - /** - * 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; + /** + * 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 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 this range is empty. + */ + public isEmpty(): boolean { + return Range.isEmpty(this); + } - /** - * 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; - } + /** + * Test if `range` is empty. + */ + public static isEmpty(range: IRange): boolean { + return range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn; + } - /** - * 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); + /** + * 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; + } - /** - * 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; - } + /** + * 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); + } - 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; - } + /** + * 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; + } - return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + /** + * 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 { + let startLineNumber: number; + let startColumn: number; + let endLineNumber: number; + let 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; } - /** - * A intersection of the two ranges. - */ - public intersectRanges(range: IRange): Range { - return Range.intersectRanges(this, range); + 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; } - /** - * 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); - } + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } - if (resultEndLineNumber > otherEndLineNumber) { - resultEndLineNumber = otherEndLineNumber; - resultEndColumn = otherEndColumn; - } else if (resultEndLineNumber === otherEndLineNumber) { - resultEndColumn = Math.min(resultEndColumn, otherEndColumn); - } + /** + * A intersection of the two ranges. + */ + public intersectRanges(range: IRange): Range | null { + return Range.intersectRanges(this, range); + } - // 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); + /** + * A intersection of the two ranges. + */ + public static intersectRanges(a: IRange, b: IRange): Range | null { + let resultStartLineNumber = a.startLineNumber; + let resultStartColumn = a.startColumn; + let resultEndLineNumber = a.endLineNumber; + let resultEndColumn = a.endColumn; + const otherStartLineNumber = b.startLineNumber; + const otherStartColumn = b.startColumn; + const otherEndLineNumber = b.endLineNumber; + const otherEndColumn = b.endColumn; + + if (resultStartLineNumber < otherStartLineNumber) { + resultStartLineNumber = otherStartLineNumber; + resultStartColumn = otherStartColumn; + } else if (resultStartLineNumber === otherStartLineNumber) { + resultStartColumn = Math.max(resultStartColumn, otherStartColumn); } - /** - * Test if this range equals other. - */ - public equalsRange(other: IRange): boolean { - return Range.equalsRange(this, other); + if (resultEndLineNumber > otherEndLineNumber) { + resultEndLineNumber = otherEndLineNumber; + resultEndColumn = otherEndColumn; + } else if (resultEndLineNumber === otherEndLineNumber) { + resultEndColumn = Math.min(resultEndColumn, otherEndColumn); } - /** - * 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 - ); + // Check if selection is now empty + if (resultStartLineNumber > resultEndLineNumber) { + return null; } - - /** - * 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); + if (resultStartLineNumber === resultEndLineNumber && resultStartColumn > resultEndColumn) { + return null; } - /** - * 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); - } + return new Range(resultStartLineNumber, resultStartColumn, resultEndLineNumber, resultEndColumn); + } - /** - * Transform to a user presentable string representation. - */ - public toString(): string { - return ( - '[' + - this.startLineNumber + - ',' + - this.startColumn + - ' -> ' + - this.endLineNumber + - ',' + - this.endColumn + - ']' - ); - } + /** + * Test if this range equals other. + */ + public equalsRange(other: IRange): boolean { + return Range.equalsRange(this, other); + } - /** - * 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); - } + /** + * 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 + ); + } - /** - * 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); - } + /** + * 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); + } - /** - * Create a new empty range using this range's start position. - */ - public collapseToStart(): Range { - return Range.collapseToStart(this); - } + /** + * 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); + } - /** - * 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); - } + /** + * 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); + } - public static fromPositions(start: vscMockPosition.IPosition, end: vscMockPosition.IPosition = start): Range { - return new Range(start.lineNumber, start.column, end.lineNumber, end.column); - } + /** + * 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 `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); - } + /** + * 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); + } + + // --- - /** - * 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' - ); + 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 | null { + if (!range) { + return null; } + return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } - /** - * 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; - } + /** + * Test if `obj` is an `IRange`. + */ + public static isIRange(obj?: { + startLineNumber: unknown; + startColumn: unknown; + endLineNumber: unknown; + endColumn: unknown; + }): obj is IRange { + return ( + obj !== undefined && + typeof obj.startLineNumber === 'number' && + typeof obj.startColumn === 'number' && + typeof obj.endLineNumber === 'number' && + typeof obj.endColumn === 'number' + ); + } - // Check if `b` is before `a` - if ( - b.endLineNumber < a.startLineNumber || - (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn) - ) { - return false; - } + /** + * 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; + } - // These ranges must intersect - return true; + // Check if `b` is before `a` + if ( + b.endLineNumber < a.startLineNumber || + (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn) + ) { + return false; } - /** - * 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; + // 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 { + const aStartLineNumber = a.startLineNumber | 0; + const bStartLineNumber = b.startLineNumber | 0; + + if (aStartLineNumber === bStartLineNumber) { + const aStartColumn = a.startColumn | 0; + const bStartColumn = b.startColumn | 0; + + if (aStartColumn === bStartColumn) { + const aEndLineNumber = a.endLineNumber | 0; + const bEndLineNumber = b.endLineNumber | 0; + + if (aEndLineNumber === bEndLineNumber) { + const aEndColumn = a.endColumn | 0; + const bEndColumn = b.endColumn | 0; + return aEndColumn - bEndColumn; } - return aStartColumn - bStartColumn; + return aEndLineNumber - bEndLineNumber; } - return aStartLineNumber - bStartLineNumber; + 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; + /** + * 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.endColumn - b.endColumn; + return a.startLineNumber - b.startLineNumber; } - return a.endLineNumber - b.endLineNumber; + 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; - } + /** + * 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 index 9e82b905f7cc..84b165f03b4c 100644 --- a/src/test/mocks/vsc/selection.ts +++ b/src/test/mocks/vsc/selection.ts @@ -1,245 +1,235 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + '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; + +import * as vscMockPosition from './position'; +import * as vscMockRange from './range'; + +/** + * 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; } /** - * The direction of a selection. + * Clone this selection. */ - export enum SelectionDirection { - /** - * The selection starts above where it ends. - */ - LTR, - /** - * The selection starts below where it ends. - */ - RTL + public clone(): Selection { + return new Selection( + this.selectionStartLineNumber, + this.selectionStartColumn, + this.positionLineNumber, + this.positionColumn, + ); } /** - * 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; - } + * Transform to a human-readable representation. + */ + public toString(): string { + return `[${this.selectionStartLineNumber},${this.selectionStartColumn} -> ${this.positionLineNumber},${this.positionColumn}]`; + } - /** - * Clone this selection. - */ - public clone(): Selection { - return new Selection( - this.selectionStartLineNumber, - this.selectionStartColumn, - this.positionLineNumber, - this.positionColumn - ); - } + /** + * Test if equals other selection. + */ + public equalsSelection(other: ISelection): boolean { + return Selection.selectionsEqual(this, other); + } - /** - * Transform to a human-readable representation. - */ - public toString(): string { - return ( - '[' + - this.selectionStartLineNumber + - ',' + - this.selectionStartColumn + - ' -> ' + - this.positionLineNumber + - ',' + - this.positionColumn + - ']' - ); - } + /** + * 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 + ); + } - /** - * Test if equals other selection. - */ - public equalsSelection(other: ISelection): boolean { - return Selection.selectionsEqual(this, other); + /** + * Get directions (LTR or RTL). + */ + public getDirection(): SelectionDirection { + if (this.selectionStartLineNumber === this.startLineNumber && this.selectionStartColumn === this.startColumn) { + return SelectionDirection.LTR; } + return SelectionDirection.RTL; + } - /** - * 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 - ); + /** + * 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 directions (LTR or RTL). - */ - public getDirection(): SelectionDirection { - if ( - this.selectionStartLineNumber === this.startLineNumber && - this.selectionStartColumn === this.startColumn - ) { - return SelectionDirection.LTR; - } - return SelectionDirection.RTL; - } + /** + * 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 `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); + /** + * 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); + } - /** - * 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, + ); + } - /** - * 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); + /** + * `a` equals `b`. + */ + public static selectionsArrEqual(a: ISelection[], b: ISelection[]): boolean { + if ((a && !b) || (!a && b)) { + return false; } - - /** - * Create a `Selection` from an `ISelection`. - */ - public static liftSelection(sel: ISelection): Selection { - return new Selection( - sel.selectionStartLineNumber, - sel.selectionStartColumn, - sel.positionLineNumber, - sel.positionColumn - ); + if (!a && !b) { + return true; } - - /** - * `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) { + if (a.length !== b.length) { + return false; + } + for (let i = 0, len = a.length; i < len; i += 1) { + if (!this.selectionsEqual(a[i], b[i])) { 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' - ); } + return true; + } - /** - * 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); - } + /** + * Test if `obj` is an `ISelection`. + */ + public static isISelection(obj?: { + selectionStartLineNumber: unknown; + selectionStartColumn: unknown; + positionLineNumber: unknown; + positionColumn: unknown; + }): obj is ISelection { + return ( + obj !== undefined && + typeof obj.selectionStartLineNumber === 'number' && + typeof obj.selectionStartColumn === 'number' && + typeof obj.positionLineNumber === 'number' && + typeof obj.positionColumn === 'number' + ); + } - return new Selection(endLineNumber, endColumn, startLineNumber, startColumn); + /** + * 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 index a1feac2d2668..571b8bc387c2 100644 --- a/src/test/mocks/vsc/strings.ts +++ b/src/test/mocks/vsc/strings.ts @@ -1,40 +1,35 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + 'use strict'; -// tslint:disable:all +/** + * Determines if haystack starts with needle. + */ +export function startsWith(haystack: string, needle: string): boolean { + if (haystack.length < needle.length) { + return false; + } -export namespace vscMockStrings { - /** - * Determines if haystack starts with needle. - */ - export function startsWith(haystack: string, needle: string): boolean { - if (haystack.length < needle.length) { + for (let i = 0; i < needle.length; i += 1) { + if (haystack[i] !== needle[i]) { return false; } + } - for (let i = 0; i < needle.length; i++) { - if (haystack[i] !== needle[i]) { - return false; - } - } + return true; +} - return true; +/** + * Determines if haystack ends with needle. + */ +export function endsWith(haystack: string, needle: string): boolean { + const diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.indexOf(needle, diff) === diff; } - - /** - * 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; - } + if (diff === 0) { + return haystack === needle; } + return false; } diff --git a/src/test/mocks/vsc/telemetryReporter.ts b/src/test/mocks/vsc/telemetryReporter.ts index d0250eb1cf5e..5df8bcac5905 100644 --- a/src/test/mocks/vsc/telemetryReporter.ts +++ b/src/test/mocks/vsc/telemetryReporter.ts @@ -3,13 +3,9 @@ 'use strict'; -// tslint:disable:all export class vscMockTelemetryReporter { - constructor() { - // - } - + // eslint-disable-next-line class-methods-use-this public sendTelemetryEvent(): void { - // + // Noop. } } diff --git a/src/test/mocks/vsc/uri.ts b/src/test/mocks/vsc/uri.ts index 6e57fee0d9c7..671c60c1ba65 100644 --- a/src/test/mocks/vsc/uri.ts +++ b/src/test/mocks/vsc/uri.ts @@ -1,226 +1,223 @@ +/* eslint-disable max-classes-per-file */ /*--------------------------------------------------------------------------------------------- * 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 */ +'use strict'; +import * as pathImport from 'path'; import { CharCode } from './charCode'; -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 isWindows = /^win/.test(process.platform); - const _schemePattern = /^\w[\w\d+.-]*$/; - const _singleSlashStart = /^\//; - const _doubleSlashStart = /^\/\//; +const _schemePattern = /^\w[\w\d+.-]*$/; +const _singleSlashStart = /^\//; +const _doubleSlashStart = /^\/\//; - let _throwOnMissingSchema: boolean = true; +const _empty = ''; +const _slash = '/'; +const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - /** - * @internal - */ - export function setUriThrowOnMissingScheme(value: boolean): boolean { - const old = _throwOnMissingSchema; - _throwOnMissingSchema = value; - return old; - } +const _pathSepMarker = isWindows ? 1 : undefined; - 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}"}`); - // // } - // } +let _throwOnMissingSchema = true; - // 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 ("//")' - ); - } +/** + * @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; +// 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) { + 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; +// 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; + default: + break; + } + return path; +} + +/** + * 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 + */ + +export class URI implements UriComponents { + static isUri(thing: unknown): thing is URI { + if (thing instanceof URI) { + return true; } - return path; + 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' + ); } - const _empty = ''; - const _slash = '/'; - const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; + /** + * scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'. + * The part before the first colon. + */ + readonly scheme: string; /** - * 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 + * 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. */ - // 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' - ); - } + readonly authority: string; - /** - * 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 - ); + /** + * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly path: string; - /** - * @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; + /** + * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly query: string; - _validateUri(this, _strict); - } + /** + * 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 = 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 ----------------------- + // ---- 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. @@ -244,91 +241,94 @@ export namespace vscUri { * 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; - } + get fsPath(): string { + // if (this.scheme !== 'file') { + // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`); + // } + return _makeFsPath(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; - } + // ---- modify to new ------------------------- - if ( - scheme === this.scheme && - authority === this.authority && - path === this.path && - query === this.query && - fragment === this.fragment - ) { - return this; - } + with(change: { + scheme?: string; + authority?: string | null; + path?: string | null; + query?: string | null; + fragment?: string | null; + }): URI { + if (!change) { + return this; + } - return new _URI(scheme, authority, path, query, fragment); + 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; } - // ---- parse & validate ------------------------ + if ( + scheme === this.scheme && + authority === this.authority && + path === this.path && + query === this.query && + fragment === this.fragment + ) { + return this; + } - /** - * 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 - ); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + 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 = false): URI { + const match = _regexp.exec(value); + if (!match) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return new _URI(_empty, _empty, _empty, _empty, _empty); } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + 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`. * @@ -349,366 +349,383 @@ export namespace vscUri { * * @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); - } + static file(path: string): URI { + let authority = _empty; - // 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; - } - } + // 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); + } - 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 - ); + // 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; + } } - // ---- printing/externalize --------------------------- + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return new _URI('file', authority, path, _empty, _empty); + } - /** - * 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); - } + static from(components: { + scheme: string; + authority?: string; + path?: string; + query?: string; + fragment?: string; + }): URI { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return new _URI( + components.scheme, + components.authority, + components.path, + components.query, + components.fragment, + ); + } - toJSON(): UriComponents { - return this; - } + // ---- printing/externalize --------------------------- - 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; - } + /** + * 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 = 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; + } + if (data instanceof URI) { + return data; } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const result = new _URI(data); + result._formatted = (<UriState>data).external; + result._fsPath = (<UriState>data)._sep === _pathSepMarker ? (<UriState>data).fsPath : null; + return result; } - export interface UriComponents { - scheme: string; - authority: string; - path: string; - query: string; - fragment: string; + 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(pathImport.join(uri.fsPath, ...pathFragment)).path; + } else { + newPath = pathImport.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; +} - interface UriState extends UriComponents { - $mid: number; - external: string; - fsPath: string; - _sep: 1 | undefined; +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 = false, + ) { + super(schemeOrData as string, authority, path, query, fragment, _strict); + this._fsPath = this.fsPath; } - 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); } - get fsPath(): string { - if (!this._fsPath) { - this._fsPath = _makeFsPath(this); + return this._fsPath; + } + + toString(skipEncoding = false): string { + if (!skipEncoding) { + if (!this._formatted) { + this._formatted = _asFormatted(this, false); } - return this._fsPath; + return this._formatted; } + // we don't cache that + return _asFormatted(this, true); + } - 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; + } +} - 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; +// 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; + let nativeEncodePos = -1; + + for (let pos = 0; pos < uriComponent.length; pos += 1) { + 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; } - if (this.query) { - res.query = this.query; + // check if we write into a new string (by default we try to return the param) + if (res !== undefined) { + res += uriComponent.charAt(pos); } - if (this.fragment) { - res.fragment = this.fragment; + } else { + // encoding needed, we need to allocate a new string + if (res === undefined) { + res = uriComponent.substr(0, pos); } - 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 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; } - // 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; - } + // 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; + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos)); } - 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 : uriComponent; +} + +function encodeURIComponentMinimal(path: string): string { + let res: string | undefined; + for (let pos = 0; pos < path.length; pos += 1) { + 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; } + 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; +/** + * 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(':'); +/** + * Create the external version of a uri + */ +function _asFormatted(uri: URI, skipEncoding: boolean): string { + const encoder = !skipEncoding ? encodeURIComponentFast : encodeURIComponentMinimal; + + let res = ''; + let { authority, path } = uri; + const { scheme, 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(authority, false); + res += encoder(userinfo, 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 - } + // <user>:<pass>@<auth> + res += encoder(userinfo.substr(0, idx), false); + res += ':'; + res += encoder(userinfo.substr(idx + 1), false); } - // encode the rest of the path - res += encoder(path, true); + res += '@'; } - if (query) { - res += '?'; - res += encoder(query, false); + 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 (fragment) { - res += '#'; - res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment; + } + 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 + } } - return res; + // 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 index 2ddb98481445..fd825440ab7d 100644 --- a/src/test/mocks/vsc/uuid.ts +++ b/src/test/mocks/vsc/uuid.ts @@ -1,11 +1,13 @@ +/* eslint-disable max-classes-per-file */ // 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. @@ -29,7 +31,6 @@ class V4UUID extends ValueUUID { 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())]; } @@ -37,7 +38,6 @@ class V4UUID extends ValueUUID { return V4UUID._oneOf(V4UUID._chars); } - // tslint:disable-next-line: member-ordering constructor() { super( [ @@ -76,8 +76,8 @@ class V4UUID extends ValueUUID { V4UUID._randomHex(), V4UUID._randomHex(), V4UUID._randomHex(), - V4UUID._randomHex() - ].join('') + V4UUID._randomHex(), + ].join(''), ); } } diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts index 5859708a8e99..c8c63b6dabe5 100644 --- a/src/test/multiRootTest.ts +++ b/src/test/multiRootTest.ts @@ -1,9 +1,8 @@ -// tslint:disable:no-console - import * as path from 'path'; -import { runTests } from 'vscode-test'; +import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { initializeLogger } from './testLogger'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; @@ -18,8 +17,8 @@ function start() { 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' } + version: getChannel(), + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Multiroot tests (with errors)', ex); process.exit(1); diff --git a/src/test/multiRootWkspc/disableLinters/.vscode/tags b/src/test/multiRootWkspc/disableLinters/.vscode/tags deleted file mode 100644 index 4739b4629cfb..000000000000 --- a/src/test/multiRootWkspc/disableLinters/.vscode/tags +++ /dev/null @@ -1,19 +0,0 @@ -!_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/multi.code-workspace b/src/test/multiRootWkspc/multi.code-workspace index 6a9901d9df65..5c90439e5546 100644 --- a/src/test/multiRootWkspc/multi.code-workspace +++ b/src/test/multiRootWkspc/multi.code-workspace @@ -25,6 +25,5 @@ "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 index 656ad4032082..0967ef424bce 100644 --- a/src/test/multiRootWkspc/parent/child/.vscode/settings.json +++ b/src/test/multiRootWkspc/parent/child/.vscode/settings.json @@ -1,3 +1 @@ -{ - "python.workspaceSymbols.enabled": true -} +{} diff --git a/src/test/multiRootWkspc/parent/child/.vscode/tags b/src/test/multiRootWkspc/parent/child/.vscode/tags deleted file mode 100644 index e6791c755b0f..000000000000 --- a/src/test/multiRootWkspc/parent/child/.vscode/tags +++ /dev/null @@ -1,24 +0,0 @@ -!_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/workspace1/.vscode/tags b/src/test/multiRootWkspc/workspace1/.vscode/tags deleted file mode 100644 index 4739b4629cfb..000000000000 --- a/src/test/multiRootWkspc/workspace1/.vscode/tags +++ /dev/null @@ -1,19 +0,0 @@ -!_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/workspace2/.vscode/settings.json b/src/test/multiRootWkspc/workspace2/.vscode/settings.json index 385728982cfa..0967ef424bce 100644 --- a/src/test/multiRootWkspc/workspace2/.vscode/settings.json +++ b/src/test/multiRootWkspc/workspace2/.vscode/settings.json @@ -1,4 +1 @@ -{ - "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace2.tags.file", - "python.workspaceSymbols.enabled": true -} +{} diff --git a/src/test/multiRootWkspc/workspace2/workspace2.tags.file b/src/test/multiRootWkspc/workspace2/workspace2.tags.file deleted file mode 100644 index 2d54e7ed7c7b..000000000000 --- a/src/test/multiRootWkspc/workspace2/workspace2.tags.file +++ /dev/null @@ -1,24 +0,0 @@ -!_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/workspace3/.vscode/settings.json b/src/test/multiRootWkspc/workspace3/.vscode/settings.json index 8779a0c08efe..0967ef424bce 100644 --- a/src/test/multiRootWkspc/workspace3/.vscode/settings.json +++ b/src/test/multiRootWkspc/workspace3/.vscode/settings.json @@ -1,3 +1 @@ -{ - "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" -} +{} diff --git a/src/test/multiRootWkspc/workspace3/workspace3.tags.file b/src/test/multiRootWkspc/workspace3/workspace3.tags.file deleted file mode 100644 index 9a141392d6ae..000000000000 --- a/src/test/multiRootWkspc/workspace3/workspace3.tags.file +++ /dev/null @@ -1,19 +0,0 @@ -!_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 index 05e3ff90627a..0067803af8f0 100644 --- a/src/test/performance/load.perf.test.ts +++ b/src/test/performance/load.perf.test.ts @@ -3,10 +3,8 @@ 'use strict'; -// tslint:disable:no-invalid-this no-console - import { expect } from 'chai'; -import * as fs from 'fs-extra'; +import * as fs from '../../client/common/platform/fs-paths'; import { EOL } from 'os'; import * as path from 'path'; import { commands, extensions } from 'vscode'; @@ -62,10 +60,10 @@ suite('Activation Times', () => { } 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!) + 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!) + JSON.parse(process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS!), ); const devActivationAvgTime = devActivationTimes.reduce((sum, item) => sum + item, 0) / devActivationTimes.length; @@ -81,7 +79,7 @@ suite('Activation Times', () => { expect(devActivationAvgTime - releaseActivationAvgTime).to.be.lessThan( AllowedIncreaseInActivationDelayInMS, - 'Activation times have increased above allowed threshold.' + 'Activation times have increased above allowed threshold.', ); }); } diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts index df2208e19481..2398f745c27a 100644 --- a/src/test/performanceTest.ts +++ b/src/test/performanceTest.ts @@ -12,16 +12,14 @@ This block of code merely launches the tests by using either the dev or release 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 fs from '../client/common/platform/fs-paths'; import * as path from 'path'; -import * as request from 'request'; +import * as bent from 'bent'; import { LanguageServerType } from '../client/activation/types'; import { EXTENSION_ROOT_DIR, PVSC_EXTENSION_ID } from '../client/common/constants'; import { unzip } from './common'; @@ -38,7 +36,7 @@ const logFilesPath = path.join(tmpFolder, 'test', 'logs'); enum Version { Dev, - Release + Release, } class TestRunner { @@ -52,7 +50,7 @@ class TestRunner { const languageServerLogFiles: string[] = []; for (let i = 0; i < timesToLoadEachVersion; i += 1) { - await this.enableLanguageServer(false); + await this.enableLanguageServer(); const devLogFile = path.join(logFilesPath, `dev_loadtimes${i}.txt`); console.log(`Start Performance Tests: Counter ${i}, for Dev version with Jedi`); @@ -63,22 +61,13 @@ class TestRunner { 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 - }" }`; + private async enableLanguageServer() { + const settings = `{ "python.languageServer": "${LanguageServerType.Jedi}" }`; await fs.writeFile(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance', 'settings.json'), settings); } @@ -89,7 +78,7 @@ class TestRunner { 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 + CODE_EXTENSIONS_PATH: version === Version.Release ? publishedExtensionPath : EXTENSION_ROOT_DIR, }; await this.launchTest(env); @@ -98,19 +87,19 @@ class TestRunner { 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) + 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) => { + await new Promise<void>((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 + ...customEnvVars, }; const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { cwd: EXTENSION_ROOT_DIR, env }); @@ -134,24 +123,15 @@ class TestRunner { 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 request = bent.default('string', 'GET', 200); + + const content: string = await request(url); 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; } @@ -163,7 +143,7 @@ class TestRunner { return destination; } - await download(url, path.dirname(destination), { filename: path.basename(destination) }); + await download.default(url, path.dirname(destination), { filename: path.basename(destination) }); return destination; } } diff --git a/src/test/proc.ts b/src/test/proc.ts index d674d29b8cc0..8a21eb379f76 100644 --- a/src/test/proc.ts +++ b/src/test/proc.ts @@ -54,7 +54,7 @@ export class Proc { this.raw = (raw as unknown) as IRawProc; this.output = output; } - public get pid(): number { + public get pid(): number | undefined { return this.raw.pid; } public get exited(): boolean { @@ -69,7 +69,7 @@ export class Proc { } this.result = { exitCode: this.raw.exitCode, - stdout: this.output.stdout + stdout: this.output.stdout, }; return this.result; } diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts index a29b78fcd4fb..55644d80ae54 100644 --- a/src/test/providers/codeActionProvider/main.unit.test.ts +++ b/src/test/providers/codeActionProvider/main.unit.test.ts @@ -3,11 +3,10 @@ '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 { CodeActionKind, 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'; @@ -25,16 +24,16 @@ suite('Code Action Provider service', async () => { registerCodeActionsProvider: ( _selector: DocumentSelector, _provider: CodeActionProvider, - _metadata: CodeActionProviderMetadata + _metadata: CodeActionProviderMetadata, ) => { selector = _selector; provider = _provider; metadata = _metadata; - } + }, }, CodeActionKind: { - QuickFix: 'CodeAction' - } + QuickFix: 'CodeAction', + }, }; rewiremock.enable(); rewiremock('vscode').with(vscodeMock); @@ -46,11 +45,10 @@ suite('Code Action Provider service', async () => { assert.deepEqual(selector!, { scheme: 'file', language: 'jsonc', - pattern: '**/launch.json' + pattern: '**/launch.json', }); assert.deepEqual(metadata!, { - // tslint:disable-next-line:no-any - providedCodeActionKinds: ['CodeAction' as any] + providedCodeActionKinds: [('CodeAction' as unknown) as CodeActionKind], }); expect(provider!).instanceOf(LaunchJsonCodeActionProvider); }); diff --git a/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts b/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts deleted file mode 100644 index e6fad21c7750..000000000000 --- a/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index b4fec804ea39..000000000000 --- a/src/test/providers/foldingProvider.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -// 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 deleted file mode 100644 index 941bb1f14ea1..000000000000 --- a/src/test/providers/importSortProvider.unit.test.ts +++ /dev/null @@ -1,559 +0,0 @@ -// 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 { ChildProcess } from 'child_process'; -import { EOL } from 'os'; -import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; -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 } 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, - IPythonSettings, - ISortImportSettings -} from '../../client/common/types'; -import { createDeferred, createDeferredFromPromise } from '../../client/common/utils/async'; -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 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>(); - 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); - }); - - 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({ throwOnStdErr: true, 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({ throwOnStdErr: true, 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(); - }); -}); diff --git a/src/test/providers/repl.unit.test.ts b/src/test/providers/repl.unit.test.ts index 31a213f3ac33..72adfa95a4a0 100644 --- a/src/test/providers/repl.unit.test.ts +++ b/src/test/providers/repl.unit.test.ts @@ -8,14 +8,15 @@ import { IActiveResourceService, ICommandManager, IDocumentManager, - IWorkspaceService + IWorkspaceService, } from '../../client/common/application/types'; import { Commands } from '../../client/common/constants'; +import { IInterpreterService } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; import { ReplProvider } from '../../client/providers/replProvider'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; 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>; @@ -24,6 +25,7 @@ suite('REPL Provider', () => { let documentManager: TypeMoq.IMock<IDocumentManager>; let activeResourceService: TypeMoq.IMock<IActiveResourceService>; let replProvider: ReplProvider; + let interpreterService: TypeMoq.IMock<IInterpreterService>; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); commandManager = TypeMoq.Mock.ofType<ICommandManager>(); @@ -34,23 +36,29 @@ suite('REPL Provider', () => { 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'))) + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))) .returns(() => codeExecutionService.object); serviceContainer.setup((c) => c.get(IDocumentManager)).returns(() => documentManager.object); serviceContainer.setup((c) => c.get(IActiveResourceService)).returns(() => activeResourceService.object); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); }); teardown(() => { try { replProvider.dispose(); - // tslint:disable-next-line:no-empty - } catch {} + } catch { + // No catch clause. + } }); 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() + TypeMoq.Times.once(), ); }); @@ -58,7 +66,7 @@ suite('REPL Provider', () => { const disposable = TypeMoq.Mock.ofType<Disposable>(); commandManager .setup((c) => - c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => disposable.object); @@ -68,13 +76,14 @@ suite('REPL Provider', () => { disposable.verify((d) => d.dispose(), TypeMoq.Times.once()); }); - test('Ensure execution is carried smoothly in the handler if there are no errors', () => { + test('Ensure execution is carried smoothly in the handler if there are no errors', async () => { const resource = Uri.parse('a'); const disposable = TypeMoq.Mock.ofType<Disposable>(); - let commandHandler: undefined | (() => void); + let commandHandler: undefined | (() => Promise<void>); + commandManager .setup((c) => - c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns((_cmd, callback) => { commandHandler = callback; @@ -87,11 +96,11 @@ suite('REPL Provider', () => { replProvider = new ReplProvider(serviceContainer.object); expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - commandHandler!.call(replProvider); + await commandHandler!.call(replProvider); serviceContainer.verify( - (c) => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), - TypeMoq.Times.once() + (c) => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard')), + 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 index 7c55645bd473..007638ab77b6 100644 --- a/src/test/providers/serviceRegistry.unit.test.ts +++ b/src/test/providers/serviceRegistry.unit.test.ts @@ -8,9 +8,7 @@ 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; @@ -21,17 +19,11 @@ suite('Common Providers Service Registry', () => { test('Ensure services are registered', async () => { registerTypes(instance(serviceManager)); - verify( - serviceManager.addSingleton<ISortImportsEditingProvider>( - ISortImportsEditingProvider, - SortImportsEditingProvider - ) - ).once(); verify( serviceManager.addSingleton<IExtensionSingleActivationService>( IExtensionSingleActivationService, - CodeActionProviderService - ) + CodeActionProviderService, + ), ).once(); }); }); diff --git a/src/test/providers/shebangCodeLenseProvider.unit.test.ts b/src/test/providers/shebangCodeLenseProvider.unit.test.ts deleted file mode 100644 index c878cb34a9fa..000000000000 --- a/src/test/providers/shebangCodeLenseProvider.unit.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -// 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 index aa1fc837cfd1..8f684835b7cf 100644 --- a/src/test/providers/terminal.unit.test.ts +++ b/src/test/providers/terminal.unit.test.ts @@ -2,39 +2,59 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import * as sinon from 'sinon'; 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 { TerminalEnvVarActivation } from '../../client/common/experiments/groups'; import { TerminalService } from '../../client/common/terminal/service'; import { ITerminalActivator, ITerminalServiceFactory } from '../../client/common/terminal/types'; -import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../client/common/types'; +import { + IConfigurationService, + IExperimentService, + IPythonSettings, + ITerminalSettings, +} from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { TerminalProvider } from '../../client/providers/terminalProvider'; +import * as extapi from '../../client/envExt/api.internal'; -// 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 experimentService: TypeMoq.IMock<IExperimentService>; let terminalProvider: TerminalProvider; + let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; const resource = Uri.parse('a'); setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + experimentService = TypeMoq.Mock.ofType<IExperimentService>(); + experimentService.setup((e) => e.inExperimentSync(TerminalEnvVarActivation.experiment)).returns(() => false); activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer.setup((c) => c.get(IExperimentService)).returns(() => experimentService.object); 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(() => { + sinon.restore(); try { terminalProvider.dispose(); - // tslint:disable-next-line:no-empty - } catch {} + } catch { + // No catch clause. + } }); test('Ensure command is registered', () => { @@ -42,7 +62,7 @@ suite('Terminal Provider', () => { commandManager.verify( (c) => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.once() + TypeMoq.Times.once(), ); }); @@ -50,7 +70,7 @@ suite('Terminal Provider', () => { const disposable = TypeMoq.Mock.ofType<Disposable>(); commandManager .setup((c) => - c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => disposable.object); @@ -65,7 +85,7 @@ suite('Terminal Provider', () => { let commandHandler: undefined | (() => void); commandManager .setup((c) => - c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns((_cmd, callback) => { commandHandler = callback; @@ -94,7 +114,6 @@ suite('Terminal Provider', () => { 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>; @@ -122,11 +141,7 @@ suite('Terminal Provider', () => { .returns(() => activeResourceService.object); terminal = TypeMoq.Mock.ofType<Terminal>(); - terminal - .setup((c) => c.creationOptions) - .returns(() => { - return { hideFromUser: false }; - }); + terminal.setup((c) => c.creationOptions).returns(() => ({ hideFromUser: false })); }); test('If terminal.activateCurrentTerminal setting is set, provided terminal should be activated', async () => { @@ -145,7 +160,7 @@ suite('Terminal Provider', () => { terminalActivator.verify( (a) => a.activateEnvironmentInTerminal(terminal.object, TypeMoq.It.isAny()), - TypeMoq.Times.once() + TypeMoq.Times.once(), ); configService.verifyAll(); activeResourceService.verifyAll(); @@ -167,7 +182,7 @@ suite('Terminal Provider', () => { terminalActivator.verify( (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never() + TypeMoq.Times.never(), ); activeResourceService.verifyAll(); configService.verifyAll(); @@ -184,18 +199,14 @@ suite('Terminal Provider', () => { .returns(() => resource) .verifiable(TypeMoq.Times.once()); - terminal - .setup((c) => c.creationOptions) - .returns(() => { - return { hideFromUser: true }; - }); + terminal.setup((c) => c.creationOptions).returns(() => ({ 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() + TypeMoq.Times.never(), ); activeResourceService.verifyAll(); configService.verifyAll(); @@ -217,7 +228,7 @@ suite('Terminal Provider', () => { terminalActivator.verify( (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never() + TypeMoq.Times.never(), ); activeResourceService.verifyAll(); configService.verifyAll(); @@ -232,7 +243,7 @@ suite('Terminal Provider', () => { try { await terminalProvider.initialize(undefined); } catch (ex) { - assert(false, `No error should be thrown, ${ex}`); + assert.ok(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..9577e7ada490 --- /dev/null +++ b/src/test/pythonEnvironments/base/common.ts @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import { Event, Uri } from 'vscode'; +import { createDeferred, flattenIterator, iterable, mapToIterator } from '../../../client/common/utils/async'; +import { getArchitecture } from '../../../client/common/utils/platform'; +import { getVersionString } from '../../../client/common/utils/version'; +import { + PythonDistroInfo, + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonExecutableInfo, +} from '../../../client/pythonEnvironments/base/info'; +import { buildEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; +import { getEmptyVersion, parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion'; +import { + BasicEnvInfo, + IPythonEnvsIterator, + isProgressEvent, + Locator, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../../client/pythonEnvironments/base/locator'; +import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; +import { noop } from '../../core'; + +export function createLocatedEnv( + locationStr: string, + versionStr: string, + kind = PythonEnvKind.Unknown, + exec: string | PythonExecutableInfo = 'python', + distro: PythonDistroInfo = { org: '' }, + searchLocation?: Uri, +): PythonEnvInfo { + const location = + locationStr === '' + ? '' // an empty location + : path.normalize(locationStr); + let executable: string | undefined; + if (typeof exec === 'string') { + const normalizedExecutable = path.normalize(exec); + executable = + location === '' || path.isAbsolute(normalizedExecutable) + ? normalizedExecutable + : path.join(location, 'bin', normalizedExecutable); + } + const version = + versionStr === '' + ? getEmptyVersion() // an empty version + : parseVersion(versionStr); + const env = buildEnvInfo({ + kind, + executable, + location, + version, + searchLocation, + }); + env.arch = getArchitecture(); + env.distro = distro; + if (typeof exec !== 'string') { + env.executable = exec; + } + return env; +} + +export function createBasicEnv( + kind: PythonEnvKind, + executablePath: string, + source?: PythonEnvSource[], + envPath?: string, +): BasicEnvInfo { + const basicEnv = { executablePath, kind, source, envPath }; + if (!source) { + delete basicEnv.source; + } + if (!envPath) { + delete basicEnv.envPath; + } + return basicEnv; +} + +export function createNamedEnv( + name: string, + versionStr: string, + kind?: PythonEnvKind, + exec: string | PythonExecutableInfo = 'python', + distro?: PythonDistroInfo, +): PythonEnvInfo { + const env = createLocatedEnv('', versionStr, kind, exec, distro); + env.name = name; + return env; +} + +export class SimpleLocator<I = PythonEnvInfo> extends Locator<I> { + public readonly providerId: string = 'SimpleLocator'; + + private deferred = createDeferred<void>(); + + constructor( + private envs: I[], + public callbacks: { + resolve?: null | ((env: PythonEnvInfo | string) => Promise<PythonEnvInfo | undefined>); + before?(): Promise<void>; + after?(): Promise<void>; + onUpdated?: Event<PythonEnvUpdatedEvent<I> | ProgressNotificationEvent>; + beforeEach?(e: I): Promise<void>; + afterEach?(e: I): Promise<void>; + onQuery?(query: PythonLocatorQuery | undefined, envs: I[]): Promise<I[]>; + } = {}, + private options?: { resolveAsString?: boolean }, + ) { + super(); + } + + public get done(): Promise<void> { + return this.deferred.promise; + } + + public fire(event: PythonEnvsChangedEvent): void { + this.emitter.fire(event); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<I> { + const { deferred } = this; + const { callbacks } = this; + let { envs } = this; + const iterator: IPythonEnvsIterator<I> = (async function* () { + if (callbacks?.onQuery !== undefined) { + envs = await callbacks.onQuery(query, envs); + } + if (callbacks.before !== undefined) { + await callbacks.before(); + } + if (callbacks.beforeEach !== undefined) { + // The results will likely come in a different order. + const mapped = mapToIterator(envs, async (env) => { + await callbacks.beforeEach!(env); + return env; + }); + for await (const env of iterable(mapped)) { + yield env; + if (callbacks.afterEach !== undefined) { + await callbacks.afterEach(env); + } + } + } else { + for (const env of envs) { + yield env; + if (callbacks.afterEach !== undefined) { + await callbacks.afterEach(env); + } + } + } + if (callbacks?.after !== undefined) { + await callbacks.after(); + } + deferred.resolve(); + })(); + iterator.onUpdated = this.callbacks?.onUpdated; + return iterator; + } + + public async resolveEnv(env: string): Promise<PythonEnvInfo | undefined> { + const envInfo: PythonEnvInfo = createLocatedEnv('', '', undefined, env); + if (this.callbacks.resolve === undefined) { + return envInfo; + } + if (this.callbacks?.resolve === null) { + return undefined; + } + return this.callbacks.resolve(this.options?.resolveAsString ? env : envInfo); + } +} + +export async function getEnvs<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I>): Promise<I[]> { + return flattenIterator(iterator); +} + +/** + * Unroll the given iterator into an array. + * + * This includes applying any received updates. + */ +export async function getEnvsWithUpdates<I = PythonEnvInfo>( + iterator: IPythonEnvsIterator<I>, + iteratorUpdateCallback: () => void = noop, +): Promise<I[]> { + const envs: (I | undefined)[] = []; + + const updatesDone = createDeferred<void>(); + if (iterator.onUpdated === undefined) { + updatesDone.resolve(); + } else { + const listener = iterator.onUpdated((event) => { + if (isProgressEvent(event)) { + if (event.stage !== ProgressReportStage.discoveryFinished) { + return; + } + updatesDone.resolve(); + listener.dispose(); + } else if (event.index !== undefined) { + const { index, update } = event; + // We don't worry about if envs[index] is set already. + envs[index] = update; + } + }); + } + + let itemIndex = 0; + for await (const env of iterator) { + // We can't just push because updates might get emitted early. + if (envs[itemIndex] === undefined) { + envs[itemIndex] = env; + } + itemIndex += 1; + } + iteratorUpdateCallback(); + await updatesDone.promise; + + // Do not return invalid environments + return envs.filter((e) => e !== undefined).map((e) => e!); +} + +export function sortedEnvs(envs: PythonEnvInfo[]): PythonEnvInfo[] { + return envs.sort((env1, env2) => { + const env1str = `${env1.kind}-${env1.executable.filename}-${getVersionString(env1.version)}`; + const env2str = `${env2.kind}-${env2.executable.filename}-${getVersionString(env2.version)}`; + return env1str.localeCompare(env2str); + }); +} + +export function assertSameEnvs(envs: PythonEnvInfo[], expected: PythonEnvInfo[]): void { + expect(sortedEnvs(envs)).to.deep.equal(sortedEnvs(expected)); +} diff --git a/src/test/pythonEnvironments/base/info/env.unit.test.ts b/src/test/pythonEnvironments/base/info/env.unit.test.ts new file mode 100644 index 000000000000..20bff8d71249 --- /dev/null +++ b/src/test/pythonEnvironments/base/info/env.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 { Uri } from 'vscode'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { parseVersionInfo } from '../../../../client/common/utils/version'; +import { PythonEnvInfo, PythonDistroInfo, PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; +import { areEnvsDeepEqual, setEnvDisplayString } from '../../../../client/pythonEnvironments/base/info/env'; +import { createLocatedEnv } from '../common'; + +suite('Environment helpers', () => { + const name = 'my-env'; + const location = 'x/y/z/spam/'; + const searchLocation = 'x/y/z'; + const arch = Architecture.x64; + const version = '3.8.1'; + const kind = PythonEnvKind.Venv; + const distro: PythonDistroInfo = { + org: 'Distro X', + defaultDisplayName: 'distroX 1.2', + version: parseVersionInfo('1.2.3')?.version, + binDir: 'distroX/bin', + }; + const locationConda1 = 'x/y/z/conda1'; + const locationConda2 = 'x/y/z/conda2'; + const kindConda = PythonEnvKind.Conda; + function getEnv(info: { + version?: string; + arch?: Architecture; + name?: string; + kind?: PythonEnvKind; + distro?: PythonDistroInfo; + display?: string; + location?: string; + searchLocation?: string; + }): PythonEnvInfo { + const env = createLocatedEnv( + info.location || '', + info.version || '', + info.kind || PythonEnvKind.Unknown, + 'python', // exec + info.distro, + info.searchLocation ? Uri.file(info.searchLocation) : undefined, + ); + env.name = info.name || ''; + env.arch = info.arch || Architecture.Unknown; + env.display = info.display; + return env; + } + function testGenerator() { + const tests: [PythonEnvInfo, string, string][] = [ + [getEnv({}), 'Python', 'Python'], + [getEnv({ version, arch, name, kind, distro }), "Python 3.8.1 ('my-env')", "Python 3.8.1 ('my-env': venv)"], + // without "suffix" info + [getEnv({ version }), 'Python 3.8.1', 'Python 3.8.1'], + [getEnv({ arch }), 'Python 64-bit', 'Python 64-bit'], + [getEnv({ version, arch }), 'Python 3.8.1 64-bit', 'Python 3.8.1 64-bit'], + // with "suffix" info + [getEnv({ name }), "Python ('my-env')", "Python ('my-env')"], + [getEnv({ kind }), 'Python', 'Python (venv)'], + [getEnv({ name, kind }), "Python ('my-env')", "Python ('my-env': venv)"], + // env.location is ignored. + [getEnv({ location }), 'Python', 'Python'], + [getEnv({ name, location }), "Python ('my-env')", "Python ('my-env')"], + [ + getEnv({ name, location, searchLocation, version, arch }), + "Python 3.8.1 64-bit ('my-env')", + "Python 3.8.1 64-bit ('my-env')", + ], + // conda env.name is empty. + [getEnv({ kind: kindConda }), 'Python', 'Python (conda)'], + [getEnv({ location: locationConda1, kind: kindConda }), "Python ('conda1')", "Python ('conda1': conda)"], + [getEnv({ location: locationConda2, kind: kindConda }), "Python ('conda2')", "Python ('conda2': conda)"], + ]; + return tests; + } + testGenerator().forEach(([env, expectedDisplay, expectedDetailedDisplay]) => { + test(`"${expectedDisplay}"`, () => { + setEnvDisplayString(env); + + assert.equal(env.display, expectedDisplay); + assert.equal(env.detailedDisplayName, expectedDetailedDisplay); + }); + }); + testGenerator().forEach(([env1, _d1, display1], index1) => { + testGenerator().forEach(([env2, _d2, display2], index2) => { + if (index1 === index2) { + test(`"${display1}" === "${display2}"`, () => { + assert.strictEqual(areEnvsDeepEqual(env1, env2), true); + }); + } else { + test(`"${display1}" !== "${display2}"`, () => { + assert.strictEqual(areEnvsDeepEqual(env1, env2), false); + }); + } + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts new file mode 100644 index 000000000000..6d0866754330 --- /dev/null +++ b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; + +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; +import { getKindDisplayName, getPrioritizedEnvKinds } from '../../../../client/pythonEnvironments/base/info/envKind'; + +const KIND_NAMES: [PythonEnvKind, string][] = [ + // We handle PythonEnvKind.Unknown separately. + [PythonEnvKind.System, 'system'], + [PythonEnvKind.MicrosoftStore, 'winStore'], + [PythonEnvKind.Pyenv, 'pyenv'], + [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Hatch, 'hatch'], + [PythonEnvKind.Pixi, 'pixi'], + [PythonEnvKind.Custom, 'customGlobal'], + [PythonEnvKind.OtherGlobal, 'otherGlobal'], + [PythonEnvKind.Venv, 'venv'], + [PythonEnvKind.VirtualEnv, 'virtualenv'], + [PythonEnvKind.VirtualEnvWrapper, 'virtualenvWrapper'], + [PythonEnvKind.Pipenv, 'pipenv'], + [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'activestate'], + [PythonEnvKind.OtherVirtual, 'otherVirtual'], +]; + +suite('pyenvs info - PyEnvKind', () => { + test('all Python env kinds are covered', () => { + assert.strictEqual( + KIND_NAMES.length, + // We ignore PythonEnvKind.Unknown. + getNamesAndValues(PythonEnvKind).length - 1, + ); + }); + + suite('getKindDisplayName()', () => { + suite('known', () => { + KIND_NAMES.forEach(([kind]) => { + if (kind === PythonEnvKind.OtherGlobal || kind === PythonEnvKind.OtherVirtual) { + return; + } + test(`check ${kind}`, () => { + const name = getKindDisplayName(kind); + + assert.notStrictEqual(name, ''); + }); + }); + }); + + suite('not known', () => { + [ + PythonEnvKind.Unknown, + PythonEnvKind.OtherGlobal, + PythonEnvKind.OtherVirtual, + // Any other kinds that don't have clear display names go here. + ].forEach((kind) => { + test(`check ${kind}`, () => { + const name = getKindDisplayName(kind); + + assert.strictEqual(name, ''); + }); + }); + }); + }); + + suite('getPrioritizedEnvKinds()', () => { + test('all Python env kinds are covered', () => { + const numPrioritized = getPrioritizedEnvKinds().length; + const numNames = getNamesAndValues(PythonEnvKind).length; + + assert.strictEqual(numPrioritized, numNames); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/info/environmentInfoService.functional.test.ts b/src/test/pythonEnvironments/base/info/environmentInfoService.functional.test.ts new file mode 100644 index 000000000000..785148f8589c --- /dev/null +++ b/src/test/pythonEnvironments/base/info/environmentInfoService.functional.test.ts @@ -0,0 +1,110 @@ +// 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 { SemVer } from 'semver'; +import { ExecutionResult } from '../../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { InterpreterInformation } from '../../../../client/pythonEnvironments/base/info/interpreter'; +import { parseVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; +import * as ExternalDep from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + EnvironmentInfoServiceQueuePriority, + getEnvironmentInfoService, +} from '../../../../client/pythonEnvironments/base/info/environmentInfoService'; +import { buildEnvInfo } from '../../../../client/pythonEnvironments/base/info/env'; +import { Conda, CONDA_RUN_VERSION } from '../../../../client/pythonEnvironments/common/environmentManagers/conda'; + +suite('Environment Info Service', () => { + let stubShellExec: sinon.SinonStub; + let disposables: IDisposableRegistry; + + function createExpectedEnvInfo(executable: string): InterpreterInformation { + return { + version: { + ...parseVersion('3.8.3-final'), + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + }, + arch: Architecture.x64, + executable: { + filename: executable, + sysPrefix: 'path', + mtime: -1, + ctime: -1, + }, + }; + } + + setup(() => { + disposables = []; + stubShellExec = sinon.stub(ExternalDep, 'shellExecute'); + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + stderr: 'Some std error', // This should be ignored. + }); + }), + ); + sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + }); + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + test('Add items to queue and get results', async () => { + const envService = getEnvironmentInfoService(disposables); + const promises: Promise<InterpreterInformation | undefined>[] = []; + const expected: InterpreterInformation[] = []; + for (let i = 0; i < 10; i = i + 1) { + const path = `any-path${i}`; + if (i < 5) { + promises.push(envService.getEnvironmentInfo(buildEnvInfo({ executable: path }))); + } else { + promises.push( + envService.getEnvironmentInfo( + buildEnvInfo({ executable: 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 = getEnvironmentInfoService(disposables); + const promises: Promise<InterpreterInformation | undefined>[] = []; + const expected: InterpreterInformation[] = []; + + const path = 'any-path'; + // Clear call counts + stubShellExec.resetHistory(); + // Evaluate once so the result is cached. + await envService.getEnvironmentInfo(buildEnvInfo({ executable: path })); + + for (let i = 0; i < 10; i = i + 1) { + promises.push(envService.getEnvironmentInfo(buildEnvInfo({ executable: 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/base/info/pythonVersion.unit.test.ts b/src/test/pythonEnvironments/base/info/pythonVersion.unit.test.ts new file mode 100644 index 000000000000..620fb15f8614 --- /dev/null +++ b/src/test/pythonEnvironments/base/info/pythonVersion.unit.test.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; + +import { PythonReleaseLevel, PythonVersion } from '../../../../client/pythonEnvironments/base/info'; +import { + compareSemVerLikeVersions, + getEmptyVersion, + getShortVersionString, + parseVersion, +} from '../../../../client/pythonEnvironments/base/info/pythonVersion'; + +export function ver( + major: number, + minor: number | undefined, + micro: number | undefined, + level?: string, + serial?: number, +): PythonVersion { + const version: PythonVersion = { + major, + minor: minor === undefined ? -1 : minor, + micro: micro === undefined ? -1 : micro, + release: undefined, + }; + if (level !== undefined) { + version.release = { + serial: serial!, + level: level as PythonReleaseLevel, + }; + } + return version; +} + +const VERSION_STRINGS: [string, PythonVersion][] = [ + ['0.9.2b2', ver(0, 9, 2, 'beta', 2)], + ['3.3.1', ver(3, 3, 1)], // final + ['3.9.0rc1', ver(3, 9, 0, 'candidate', 1)], + ['2.7.11a3', ver(2, 7, 11, 'alpha', 3)], +]; + +suite('pyenvs info - getShortVersionString', () => { + for (const data of VERSION_STRINGS) { + const [expected, info] = data; + test(`conversion works for '${expected}'`, () => { + const result = getShortVersionString(info); + + assert.strictEqual(result, expected); + }); + } + + test('conversion works for final', () => { + const expected = '3.3.1'; + const info = ver(3, 3, 1, 'final', 0); + + const result = getShortVersionString(info); + + assert.strictEqual(result, expected); + }); +}); + +suite('pyenvs info - parseVersion', () => { + suite('full versions (short)', () => { + VERSION_STRINGS.forEach((data) => { + const [text, expected] = data; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + suite('full versions (long)', () => { + [ + ['0.9.2-beta2', ver(0, 9, 2, 'beta', 2)], + ['3.3.1-final', ver(3, 3, 1, 'final', 0)], + ['3.3.1-final0', ver(3, 3, 1, 'final', 0)], + ['3.9.0-candidate1', ver(3, 9, 0, 'candidate', 1)], + ['2.7.11-alpha3', ver(2, 7, 11, 'alpha', 3)], + ['0.9.2.beta.2', ver(0, 9, 2, 'beta', 2)], + ['3.3.1.final.0', ver(3, 3, 1, 'final', 0)], + ['3.9.0.candidate.1', ver(3, 9, 0, 'candidate', 1)], + ['2.7.11.alpha.3', ver(2, 7, 11, 'alpha', 3)], + ].forEach((data) => { + const [text, expected] = data as [string, PythonVersion]; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + suite('partial versions', () => { + [ + ['3.7.1', ver(3, 7, 1)], + ['3.7', ver(3, 7, -1)], + ['3', ver(3, -1, -1)], + ['37', ver(3, 7, -1)], // not 37 + ['371', ver(3, 71, -1)], // not 3.7.1 + ['3102', ver(3, 102, -1)], // not 3.10.2 + ['2.7', ver(2, 7, -1)], + ['2', ver(2, -1, -1)], // not 2.7 + ['27', ver(2, 7, -1)], + ].forEach((data) => { + const [text, expected] = data as [string, PythonVersion]; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + suite('other forms', () => { + [ + // prefixes + ['python3', ver(3, -1, -1)], + ['python3.8', ver(3, 8, -1)], + ['python3.8.1', ver(3, 8, 1)], + ['python3.8.1b2', ver(3, 8, 1, 'beta', 2)], + ['python-3', ver(3, -1, -1)], + // release ignored (missing micro) + ['python3.8b2', ver(3, 8, -1)], + ['python38b2', ver(3, 8, -1)], + ['python381b2', ver(3, 81, -1)], // not 3.8.1 + // suffixes + ['python3.exe', ver(3, -1, -1)], + ['python3.8.exe', ver(3, 8, -1)], + ['python3.8.1.exe', ver(3, 8, 1)], + ['python3.8.1b2.exe', ver(3, 8, 1, 'beta', 2)], + ['3.8.1.build123.revDEADBEEF', ver(3, 8, 1)], + ['3.8.1b2.build123.revDEADBEEF', ver(3, 8, 1, 'beta', 2)], + // dirnames + ['/x/y/z/python38/bin/python', ver(3, 8, -1)], + ['/x/y/z/python/38/bin/python', ver(3, 8, -1)], + ['/x/y/z/python/38/bin/python', ver(3, 8, -1)], + ].forEach((data) => { + const [text, expected] = data as [string, PythonVersion]; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + test('empty string results in empty version', () => { + const expected = getEmptyVersion(); + + const result = parseVersion(''); + + assert.deepEqual(result, expected); + }); + + suite('bogus input', () => { + [ + // errant dots + 'py.3.7', + 'py3.7.', + 'python.3', + // no version + 'spam', + 'python.exe', + 'python', + ].forEach((text) => { + test(`conversion does not work for '${text}'`, () => { + assert.throws(() => parseVersion(text)); + }); + }); + }); +}); + +suite('pyenvs info - compareSemVerLikeVersions', () => { + const testData = [ + { + v1: { major: 2, minor: 7, patch: 19 }, + v2: { major: 3, minor: 7, patch: 4 }, + expected: -1, + }, + { + v1: { major: 2, minor: 7, patch: 19 }, + v2: { major: 2, minor: 7, patch: 19 }, + expected: 0, + }, + { + v1: { major: 3, minor: 7, patch: 4 }, + v2: { major: 2, minor: 7, patch: 19 }, + expected: 1, + }, + { + v1: { major: 3, minor: 8, patch: 1 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: -1, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: 0, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 8, patch: 1 }, + expected: 1, + }, + { + v1: { major: 3, minor: 9, patch: 0 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: -1, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: 0, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 9, patch: 0 }, + expected: 1, + }, + ]; + + testData.forEach((data) => { + test(`Compare versions ${JSON.stringify(data.v1)} and ${JSON.stringify(data.v2)}`, () => { + const actual = compareSemVerLikeVersions(data.v1, data.v2); + assert.deepStrictEqual(actual, data.expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts new file mode 100644 index 000000000000..8e4bc02e4797 --- /dev/null +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import { EventEmitter, Uri } from 'vscode'; +import { getValues as getEnumValues } from '../../../client/common/utils/enum'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { copyEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; +import { + IPythonEnvsIterator, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../../client/pythonEnvironments/base/locator'; +import { getEnvs, getQueryFilter } from '../../../client/pythonEnvironments/base/locatorUtils'; +import { createLocatedEnv, createNamedEnv } from './common'; + +const homeDir = path.normalize('/home/me'); +const workspaceRoot = Uri.file('workspace-root'); +const doesNotExist = Uri.file(path.normalize('does-not-exist')); + +function setSearchLocation(env: PythonEnvInfo, location?: string): void { + const locationStr = location === undefined ? path.dirname(env.location) : path.normalize(location); + env.searchLocation = Uri.file(locationStr); +} + +const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.System, '/usr/bin/python3.8'); +const env2 = createNamedEnv('env2', '3.8.1rc2', PythonEnvKind.Pyenv, '/pyenv/3.8.1rc2/bin/python'); +const env3 = createNamedEnv('env3', '3.9.1b2', PythonEnvKind.Unknown, 'python3.9'); +const env4 = createNamedEnv('env4', '2.7.11', PythonEnvKind.Pyenv, '/pyenv/2.7.11/bin/python'); +const env5 = createNamedEnv('env5', '2.7', PythonEnvKind.System, 'python2'); +const env6 = createNamedEnv('env6', '3.7.4', PythonEnvKind.Conda, 'python'); +const plainEnvs = [env1, env2, env3, env4, env5, env6]; + +const envL1 = createLocatedEnv('/.venvs/envL1', '3.9.0', PythonEnvKind.Venv); +const envL2 = createLocatedEnv('/conda/envs/envL2', '3.8.3', PythonEnvKind.Conda); +const locatedEnvs = [envL1, envL2]; + +const envS1 = createNamedEnv('env S1', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir/bin/python`); +setSearchLocation(envS1, `${homeDir}/`); // Have a search location ending in '/' +const envS2 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir2/bin/python`); +setSearchLocation(envS2, homeDir); +const envS3 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${workspaceRoot.fsPath}/p/python`); +envS3.searchLocation = workspaceRoot; +const rootedEnvs = [envS1, envS2, envS3]; + +const envSL1 = createLocatedEnv(`${homeDir}/.venvs/envSL1`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL1); +const envSL2 = createLocatedEnv(`${workspaceRoot.fsPath}/.venv`, '3.8.2', PythonEnvKind.Pipenv); +setSearchLocation(envSL2); +const envSL3 = createLocatedEnv(`${homeDir}/.conda-envs/envSL3`, '3.8.2', PythonEnvKind.Conda); +setSearchLocation(envSL3); +const envSL4 = createLocatedEnv('/opt/python3.10', '3.10.0a1', PythonEnvKind.Custom); +setSearchLocation(envSL4); +const envSL5 = createLocatedEnv(`${homeDir}/.venvs/envSL5`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL5); +const rootedLocatedEnvs = [envSL1, envSL2, envSL3, envSL4, envSL5]; + +const envs = [...plainEnvs, ...locatedEnvs, ...rootedEnvs, ...rootedLocatedEnvs]; + +suite('Python envs locator utils - getQueryFilter', () => { + suite('empty query', () => { + const queries: PythonLocatorQuery[] = [ + {}, + { kinds: [] }, + // Any "defined" value for searchLocations causes filtering... + ]; + queries.forEach((query) => { + test(`all envs kept (query ${query})`, () => { + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + }); + + suite('kinds', () => { + test('match none', () => { + const query: PythonLocatorQuery = { kinds: [PythonEnvKind.Poetry] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + ([ + [PythonEnvKind.Unknown, [env3]], + [PythonEnvKind.System, [env1, env5]], + [PythonEnvKind.MicrosoftStore, []], + [PythonEnvKind.Pyenv, [env2, env4]], + [PythonEnvKind.Venv, [envL1, envSL1, envSL5]], + [PythonEnvKind.Conda, [env6, envL2, envSL3]], + ] as [PythonEnvKind, PythonEnvInfo[]][]).forEach(([kind, expected]) => { + test(`match some (one kind: ${kind})`, () => { + const query: PythonLocatorQuery = { kinds: [kind] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + test('match some (many kinds)', () => { + const expected = [env6, envL1, envL2, envSL1, envSL2, envSL3, envSL4, envSL5]; + const kinds = [ + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnv, + PythonEnvKind.Pipenv, + PythonEnvKind.Conda, + PythonEnvKind.Custom, + ]; + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + + suite('searchLocations', () => { + test('match none', () => { + const query: PythonLocatorQuery = { + searchLocations: { + roots: [doesNotExist], + doNotIncludeNonRooted: true, + }, + }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match one (multiple locations)', () => { + const expected = [envSL4]; + const searchLocations = { + roots: [ + envSL4.searchLocation!, + doesNotExist, + envSL4.searchLocation!, // repeated + ], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (one location)', () => { + const expected = [envS3, envSL2]; + const searchLocations = { + roots: [workspaceRoot], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test("match multiple (one location) uri path ending in '/'", () => { + const expected = [envS3, envSL2]; + const searchLocations = { + roots: [Uri.file(`${workspaceRoot.path}/`)], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (multiple locations)', () => { + const expected = [envS3, ...rootedLocatedEnvs]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (include non-searched envs)', () => { + const expected = [...plainEnvs, ...locatedEnvs, envS3, ...rootedLocatedEnvs]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + doNotIncludeNonRooted: false, + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all searched', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all (including non-searched)', () => { + const expected = envs; + const searchLocations = { + roots: expected.map((e) => e.searchLocation!).filter((e) => !!e), + doNotIncludeNonRooted: false, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all searched under one root', () => { + const expected = [envS1, envS2, envSL1, envSL3, envSL5]; + const searchLocations = { + roots: [Uri.file(homeDir)], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (empty roots)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [], + doNotIncludeNonRooted: false, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (with unmatched location)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [doesNotExist], + doNotIncludeNonRooted: false, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('include non rooted envs by default', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [doesNotExist], + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + suite('mixed query', () => { + test('match none', () => { + const query: PythonLocatorQuery = { + kinds: [PythonEnvKind.OtherGlobal], + searchLocations: { + roots: [doesNotExist], + }, + }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match some', () => { + const expected = [envSL1, envSL4, envSL5]; + const kinds = [PythonEnvKind.Venv, PythonEnvKind.Custom]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); +}); + +suite('Python envs locator utils - getEnvs', () => { + test('empty, no update emitter', async () => { + const iterator = (async function* () { + // Yield nothing. + })() as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('empty, with unused update emitter', async () => { + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + // eslint-disable-next-line require-yield + const iterator = (async function* () { + // Yield nothing. + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('yield one, no update emitter', async () => { + const iterator = (async function* () { + yield env1; + })() as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, no update', async () => { + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + yield env1; + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, with update', async () => { + const expected = [envSL2]; + const old = copyEnvInfo(envSL2, { kind: PythonEnvKind.Venv }); + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + yield old; + emitter.fire({ index: 0, old, update: envSL2 }); + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, no update emitter', async () => { + const expected = rootedLocatedEnvs; + const iterator = (async function* () { + yield* expected; + })() as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, none updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + yield* expected; + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, some updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + const original = [...expected]; + const updated = [1, 2, 4]; + const kind = PythonEnvKind.Unknown; + updated.forEach((index) => { + original[index] = copyEnvInfo(expected[index], { kind }); + }); + + yield* original; + + updated.forEach((index) => { + emitter.fire({ index, old: original[index], update: expected[index] }); + }); + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, all updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + const kind = PythonEnvKind.Unknown; + const original = expected.map((env) => copyEnvInfo(env, { kind })); + + yield original[0]; + yield original[1]; + emitter.fire({ index: 0, old: original[0], update: expected[0] }); + yield* original.slice(2); + original.forEach((old, index) => { + if (index > 0) { + emitter.fire({ index, old, update: expected[index] }); + } + }); + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); +}); 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..ad17b588c48b --- /dev/null +++ b/src/test/pythonEnvironments/base/locators.unit.test.ts @@ -0,0 +1,189 @@ +// 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 { Locators } from '../../../client/pythonEnvironments/base/locators'; +import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; +import { createLocatedEnv, createNamedEnv, 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 = createNamedEnv('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 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('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: { roots: [Uri.file('???')] }, + }; + let query: PythonLocatorQuery | undefined; + async function onQuery(q: PythonLocatorQuery | undefined, e: PythonEnvInfo[]) { + query = q; + return e; + } + const env1 = createNamedEnv('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 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.Custom); + const env7 = createNamedEnv('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 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('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); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts new file mode 100644 index 000000000000..9fe481c4da3f --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -0,0 +1,656 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { assert, expect } from 'chai'; +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { EventEmitter, Uri } from 'vscode'; +import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; +import { createDeferred, createDeferredFromPromise, sleep } from '../../../../../client/common/utils/async'; +import { PythonEnvInfo, PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { areSameEnv, buildEnvInfo } from '../../../../../client/pythonEnvironments/base/info/env'; +import { + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, +} from '../../../../../client/pythonEnvironments/base/locator'; +import { createCollectionCache } from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionCache'; +import { EnvsCollectionService } from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionService'; +import { PythonEnvCollectionChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { noop } from '../../../../core'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { SimpleLocator } from '../../common'; +import { assertEnvEqual, assertEnvsEqual, createFile, deleteFile } from '../envTestUtils'; +import { OSType, getOSType } from '../../../../common'; +import * as nativeFinder from '../../../../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; + +class MockNativePythonFinder implements nativeFinder.NativePythonFinder { + find(_searchPath: string): Promise<nativeFinder.NativeEnvInfo[]> { + throw new Error('Method not implemented.'); + } + + getCondaInfo(): Promise<nativeFinder.NativeCondaInfo> { + throw new Error('Method not implemented.'); + } + + resolve(_executable: string): Promise<nativeFinder.NativeEnvInfo> { + throw new Error('Method not implemented.'); + } + + refresh(): AsyncIterable<nativeFinder.NativeEnvInfo> { + const envs: nativeFinder.NativeEnvInfo[] = []; + return (async function* () { + for (const env of envs) { + yield env; + } + })(); + } + + dispose() { + /** noop */ + } +} + +suite('Python envs locator - Environments Collection', async () => { + let getNativePythonFinderStub: sinon.SinonStub; + let collectionService: EnvsCollectionService; + let storage: PythonEnvInfo[]; + + const updatedName = 'updatedName'; + const pathToCondaPython = getOSType() === OSType.Windows ? 'python.exe' : path.join('bin', 'python'); + const condaEnvWithoutPython = createEnv( + 'python', + undefined, + undefined, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython'), + PythonEnvKind.Conda, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython', pathToCondaPython), + ); + const condaEnvWithPython = createEnv( + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython', pathToCondaPython), + undefined, + undefined, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython'), + PythonEnvKind.Conda, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython', pathToCondaPython), + ); + + function applyChangeEventToEnvList(envs: PythonEnvInfo[], event: PythonEnvCollectionChangedEvent) { + const env = event.old ?? event.new; + let envIndex = -1; + if (env) { + envIndex = envs.findIndex((item) => item.executable.filename === env.executable.filename); + } + if (event.new) { + if (envIndex === -1) { + envs.push(event.new); + } else { + envs[envIndex] = event.new; + } + } + if (envIndex !== -1 && event.new === undefined) { + envs.splice(envIndex, 1); + } + return envs; + } + + function createEnv( + executable: string, + searchLocation?: Uri, + name?: string, + location?: string, + kind?: PythonEnvKind, + id?: string, + ) { + const env = buildEnvInfo({ executable, searchLocation, name, location, kind }); + env.id = id ?? env.id; + env.version.major = 3; + env.version.minor = 10; + env.version.micro = 10; + return env; + } + + function getLocatorEnvs() { + const env1 = createEnv(path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe')); + const env2 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), + Uri.file(TEST_LAYOUT_ROOT), + ); + const env3 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pyenv2', '.pyenv', 'pyenv-win', 'versions', '3.6.9', 'bin', 'python.exe'), + ); + const env4 = createEnv(path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe')); // Path is valid but it's an invalid env + return [env1, env2, env3, env4]; + } + + function getValidCachedEnvs() { + const cachedEnvForWorkspace = createEnv( + path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1', 'win1', 'python.exe'), + Uri.file(path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1')), + ); + const fakeLocalAppDataPath = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const envCached1 = createEnv(path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', 'python.exe')); + const envCached2 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), + Uri.file(TEST_LAYOUT_ROOT), + ); + const envCached3 = condaEnvWithoutPython; + return [cachedEnvForWorkspace, envCached1, envCached2, envCached3]; + } + + function getCachedEnvs() { + const envCached3 = createEnv(path.join(TEST_LAYOUT_ROOT, 'doesNotExist')); // Invalid path, should not be reported. + return [...getValidCachedEnvs(), envCached3]; + } + + function getExpectedEnvs() { + const cachedEnvForWorkspace = createEnv( + path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1', 'win1', 'python.exe'), + Uri.file(path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1')), + ); + const env1 = createEnv(path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), undefined, updatedName); + const env2 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), + Uri.file(TEST_LAYOUT_ROOT), + updatedName, + ); + const env3 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pyenv2', '.pyenv', 'pyenv-win', 'versions', '3.6.9', 'bin', 'python.exe'), + undefined, + updatedName, + ); + // Do not include cached envs which were not yielded by the locator, unless it belongs to some workspace. + return [cachedEnvForWorkspace, env1, env2, env3]; + } + + setup(async () => { + getNativePythonFinderStub = sinon.stub(nativeFinder, 'getNativePythonFinder'); + getNativePythonFinderStub.returns(new MockNativePythonFinder()); + storage = []; + const parentLocator = new SimpleLocator(getLocatorEnvs()); + const cache = await createCollectionCache({ + get: () => getCachedEnvs(), + store: async (envs) => { + storage = envs; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + }); + + teardown(async () => { + await deleteFile(condaEnvWithPython.executable.filename); // Restore to the original state + sinon.restore(); + }); + + test('getEnvs() returns valid envs from cache', () => { + const envs = collectionService.getEnvs(); + assertEnvsEqual(envs, getValidCachedEnvs()); + }); + + test('getEnvs() uses query to filter envs before returning', () => { + // Only query for environments which are not under any roots + const envs = collectionService.getEnvs({ searchLocations: { roots: [] } }); + assertEnvsEqual( + envs, + getValidCachedEnvs().filter((e) => !e.searchLocation), + ); + }); + + test('If `ifNotTriggerredAlready` option is set and a refresh for query is already triggered, triggerRefresh() does not trigger a refresh', async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + let refreshTriggerCount = 0; + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + refreshTriggerCount += 1; + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => getCachedEnvs(), + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + await collectionService.triggerRefresh(undefined); + await collectionService.triggerRefresh(undefined, { ifNotTriggerredAlready: true }); + expect(refreshTriggerCount).to.equal(1, 'Refresh should not be triggered in case 1'); + await collectionService.triggerRefresh({ searchLocations: { roots: [] } }, { ifNotTriggerredAlready: true }); + expect(refreshTriggerCount).to.equal(1, 'Refresh should not be triggered in case 2'); + await collectionService.triggerRefresh(undefined); + expect(refreshTriggerCount).to.equal(2, 'Refresh should be triggered in case 3'); + }); + + test('Ensure correct events are fired when collection changes on refresh', async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await collectionService.triggerRefresh(); + + let envs = cachedEnvs; + // Ensure when all the events are applied to the original list in sequence, the final list is as expected. + events.forEach((e) => { + envs = applyChangeEventToEnvList(envs, e); + }); + const expected = getExpectedEnvs(); + assertEnvsEqual(envs, expected); + }); + + test("Ensure update events are not fired if an environment isn't actually updated", async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + let events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await collectionService.triggerRefresh(); + expect(events.length).to.not.equal(0, 'Atleast event should be fired'); + const envs = collectionService.getEnvs(); + + // Trigger a refresh again. + events = []; + await collectionService.triggerRefresh(); + // Filter out the events which are related to envs in the cache, we expect no such events to be fired as no + // envs were updated. + events = events.filter((e) => + envs.some((env) => { + const eventEnv = e.old ?? e.new; + if (!eventEnv) { + return true; + } + return areSameEnv(eventEnv, env); + }), + ); + expect(events.length).to.equal(0, 'Do not fire additional events as envs have not updated'); + }); + + test('triggerRefresh() refreshes the collection with any new envs & removes cached envs if not relevant', async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await collectionService.triggerRefresh(); + + let envs = cachedEnvs; + // Ensure when all the events are applied to the original list in sequence, the final list is as expected. + events.forEach((e) => { + envs = applyChangeEventToEnvList(envs, e); + }); + const expected = getExpectedEnvs(); + assertEnvsEqual(envs, expected); + const queriedEnvs = collectionService.getEnvs(); + assertEnvsEqual(queriedEnvs, expected); + assertEnvsEqual(storage, expected); + }); + + test('Ensure progress stage updates are emitted correctly and refresh promises correct track promise for each stage', async () => { + // Arrange + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const waitUntilEventVerified = createDeferred<void>(); + const waitForAllPathsDiscoveredEvent = createDeferred<void>(); + const parentLocator = new SimpleLocator(locatedEnvs, { + before: async () => { + onUpdated.fire({ stage: ProgressReportStage.discoveryStarted }); + }, + onUpdated: onUpdated.event, + after: async () => { + onUpdated.fire({ stage: ProgressReportStage.allPathsDiscovered }); + waitForAllPathsDiscoveredEvent.resolve(); + await waitUntilEventVerified.promise; + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + let stage: ProgressReportStage | undefined; + collectionService.onProgress((e) => { + stage = e.stage; + }); + + // Act + const discoveryPromise = collectionService.triggerRefresh(); + + // Verify stages and refresh promises + expect(stage).to.equal(ProgressReportStage.discoveryStarted, 'Discovery should already be started'); + let refreshPromise = collectionService.getRefreshPromise({ + stage: ProgressReportStage.discoveryStarted, + }); + expect(refreshPromise).to.equal(undefined); + refreshPromise = collectionService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + expect(refreshPromise).to.not.equal(undefined); + const allPathsDiscoveredPromise = createDeferredFromPromise(refreshPromise!); + refreshPromise = collectionService.getRefreshPromise({ stage: ProgressReportStage.discoveryFinished }); + expect(refreshPromise).to.not.equal(undefined); + const discoveryFinishedPromise = createDeferredFromPromise(refreshPromise!); + + expect(allPathsDiscoveredPromise.resolved).to.equal(false); + await waitForAllPathsDiscoveredEvent.promise; // Wait for all paths to be discovered. + expect(stage).to.equal(ProgressReportStage.allPathsDiscovered); + expect(allPathsDiscoveredPromise.resolved).to.equal(true); + waitUntilEventVerified.resolve(); + + await discoveryPromise; + expect(stage).to.equal(ProgressReportStage.discoveryFinished); + expect(discoveryFinishedPromise.resolved).to.equal( + true, + 'Any previous refresh promises should be resolved when refresh is over', + ); + expect(collectionService.getRefreshPromise()).to.equal( + undefined, + 'Should be undefined if no refresh is currently going on', + ); + + // Test stage when query is provided. + collectionService.onProgress((e) => { + if (e.stage === ProgressReportStage.allPathsDiscovered) { + assert(false, 'All paths discovered event should not be fired if a query is provided'); + } + }); + collectionService + .triggerRefresh({ searchLocations: { roots: [], doNotIncludeNonRooted: true } }) + .ignoreErrors(); + refreshPromise = collectionService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + expect(refreshPromise).to.equal(undefined, 'All paths discovered stage not applicable if a query is provided'); + }); + + test('resolveEnv() uses cache if complete and up to date info is available', async () => { + const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + const cachedEnvs = getCachedEnvs(); + const env = cachedEnvs[0]; + env.executable.ctime = 100; + env.executable.mtime = 100; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + const parentLocator = new SimpleLocator([], { + resolve: async (e: any) => { + if (env.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(env.executable.filename); + assertEnvEqual(resolved, env); + }); + + test('resolveEnv() does not use cache if complete info is not available', async () => { + const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + const deferred = createDeferred<void>(); + const waitDeferred = createDeferred<void>(); + const locatedEnvs = getLocatorEnvs(); + const env = locatedEnvs[0]; + env.executable.ctime = 100; + env.executable.mtime = 100; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + const parentLocator = new SimpleLocator(locatedEnvs, { + after: async () => { + waitDeferred.resolve(); + await deferred.promise; + }, + resolve: async (e: any) => { + if (env.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => [], + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + collectionService.triggerRefresh().ignoreErrors(); + await waitDeferred.promise; // Cache should already contain `env` at this point, although it is not complete. + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(env.executable.filename); + assertEnvEqual(resolved, resolvedViaLocator); + }); + + test('resolveEnv() uses underlying locator if cache does not have up to date info for env', async () => { + const cachedEnvs = getCachedEnvs(); + const env = cachedEnvs[0]; + const resolvedViaLocator = buildEnvInfo({ + executable: env.executable.filename, + sysPrefix: 'Resolved via locator', + }); + env.executable.ctime = 101; + env.executable.mtime = 90; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + const parentLocator = new SimpleLocator([], { + resolve: async (e: any) => { + if (env.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(env.executable.filename); + assertEnvEqual(resolved, resolvedViaLocator); + }); + + test('resolveEnv() adds env to cache after resolving using downstream locator', async () => { + const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + const parentLocator = new SimpleLocator([], { + resolve: async (e: any) => { + if (resolvedViaLocator.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => [], + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(resolvedViaLocator.executable.filename); + const envs = collectionService.getEnvs(); + assertEnvsEqual(envs, [resolved]); + }); + + test('resolveEnv() uses underlying locator once conda envs without python get a python installed', async () => { + const cachedEnvs = [condaEnvWithoutPython]; + const parentLocator = new SimpleLocator( + [], + { + resolve: async (e) => { + if (condaEnvWithoutPython.location === (e as string)) { + return condaEnvWithPython; + } + return undefined; + }, + }, + { resolveAsString: true }, + ); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + let resolved = await collectionService.resolveEnv(condaEnvWithoutPython.location); + assertEnvEqual(resolved, condaEnvWithoutPython); // Ensure cache is used to resolve such envs. + + condaEnvWithPython.executable.ctime = 100; + condaEnvWithPython.executable.mtime = 100; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await createFile(condaEnvWithPython.executable.filename); // Install Python into the env + + resolved = await collectionService.resolveEnv(condaEnvWithoutPython.location); + assertEnvEqual(resolved, condaEnvWithPython); // Ensure it resolves latest info. + + // Verify conda env without python in cache is replaced with updated info. + const envs = collectionService.getEnvs(); + assertEnvsEqual(envs, [condaEnvWithPython]); + + expect(events.length).to.equal(1, 'Update event should be fired'); + }); + + test('Ensure events from downstream locators do not trigger new refreshes if a refresh is already scheduled', async () => { + const refreshDeferred = createDeferred(); + let refreshCount = 0; + const parentLocator = new SimpleLocator([], { + after: () => { + refreshCount += 1; + return refreshDeferred.promise; + }, + }); + const cache = await createCollectionCache({ + get: () => [], + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + const downstreamEvents = [ + { type: FileChangeType.Created, searchLocation: Uri.file('folder1s') }, + { type: FileChangeType.Changed }, + { type: FileChangeType.Deleted, kind: PythonEnvKind.Venv }, + { type: FileChangeType.Deleted, kind: PythonEnvKind.VirtualEnv }, + ]; // Total of 4 events + await Promise.all( + downstreamEvents.map(async (event) => { + parentLocator.fire(event); + await sleep(1); // Wait for refreshes to be initialized via change events + }), + ); + + refreshDeferred.resolve(); + await sleep(1); + + await collectionService.getRefreshPromise(); // Wait for refresh to finish + + /** + * We expect 2 refreshes to be triggered in total, explanation: + * * First event triggers a refresh. + * * Second event schedules a refresh to happen once the first refresh is finished. + * * Third event is received. A fresh refresh is already scheduled to take place so no need to schedule another one. + * * Same with the fourth event. + */ + expect(refreshCount).to.equal(2); + expect(events.length).to.equal(downstreamEvents.length, 'All 4 events should also be fired by the collection'); + assert.deepStrictEqual( + events.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), + downstreamEvents.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), + ); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts new file mode 100644 index 000000000000..a7f44abbbf94 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import { PythonEnvsReducer } from '../../../../../client/pythonEnvironments/base/locators/composite/envsReducer'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { createBasicEnv, getEnvs, getEnvsWithUpdates, SimpleLocator } from '../../common'; +import { + BasicEnvInfo, + ProgressReportStage, + isProgressEvent, +} from '../../../../../client/pythonEnvironments/base/locator'; +import { createDeferred } from '../../../../../client/common/utils/async'; + +suite('Python envs locator - Environments Reducer', () => { + suite('iterEnvs()', () => { + test('Iterator only yields unique environments', async () => { + const env1 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createBasicEnv(PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env3 = createBasicEnv(PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createBasicEnv(PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 + const env5 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1 + const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments + const parentLocator = new SimpleLocator(environmentsToBeIterated); + const reducer = new PythonEnvsReducer(parentLocator); + + const iterator = reducer.iterEnvs(); + const envs = await getEnvs(iterator); + + const expected = [env1, env2, env3]; + assertBasicEnvsEqual(envs, expected); + }); + + test('Updates are applied correctly', async () => { + const env1 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createBasicEnv(PythonEnvKind.System, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.PathEnvVar, + ]); + const env3 = createBasicEnv(PythonEnvKind.Conda, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.WindowsRegistry, + ]); // Same as env2 + const env4 = createBasicEnv(PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 + const env5 = createBasicEnv(PythonEnvKind.Poetry, path.join('path', 'to', 'exec1')); // Same as env1 + const env6 = createBasicEnv(PythonEnvKind.VirtualEnv, path.join('path', 'to', 'exec1')); // Same as env1 + const environmentsToBeIterated = [env1, env2, env3, env4, env5, env6]; // Contains 3 unique environments + const parentLocator = new SimpleLocator(environmentsToBeIterated); + const reducer = new PythonEnvsReducer(parentLocator); + + const iterator = reducer.iterEnvs(); + const envs = await getEnvsWithUpdates(iterator); + + const expected = [ + createBasicEnv(PythonEnvKind.Poetry, path.join('path', 'to', 'exec1')), + createBasicEnv(PythonEnvKind.Conda, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.PathEnvVar, + PythonEnvSource.WindowsRegistry, + ]), + ]; + assertBasicEnvsEqual(envs, expected); + }); + + test('Ensure progress updates are emitted correctly', async () => { + // Arrange + const env1 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createBasicEnv(PythonEnvKind.System, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.PathEnvVar, + ]); + const envsReturnedByParentLocator = [env1, env2]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const reducer = new PythonEnvsReducer(parentLocator); + + // Act + const iterator = reducer.iterEnvs(); + let stage: ProgressReportStage | undefined; + let waitForProgressEvent = createDeferred<void>(); + iterator.onUpdated!(async (event) => { + if (isProgressEvent(event)) { + stage = event.stage; + waitForProgressEvent.resolve(); + } + }); + // Act + let result = await iterator.next(); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryStarted); + + // Act + waitForProgressEvent = createDeferred<void>(); + while (!result.done) { + // Once all envs are iterated, discovery should be finished. + result = await iterator.next(); + } + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryFinished); + }); + }); + + test('onChanged fires iff onChanged from locator manager fires', () => { + const parentLocator = new SimpleLocator([]); + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; + const expected = [event1, event2]; + const reducer = new PythonEnvsReducer(parentLocator); + + const events: PythonEnvsChangedEvent[] = []; + reducer.onChanged((e) => events.push(e)); + + parentLocator.fire(event1); + parentLocator.fire(event2); + + assert.deepEqual(events, expected); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts new file mode 100644 index 000000000000..0d189da35282 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { EventEmitter, Uri } from 'vscode'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../../../client/common/types'; +import { Architecture } from '../../../../../client/common/utils/platform'; +import * as platformApis from '../../../../../client/common/utils/platform'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonEnvType, + PythonVersion, + UNKNOWN_PYTHON_VERSION, +} from '../../../../../client/pythonEnvironments/base/info'; +import { getEmptyVersion, parseVersion } from '../../../../../client/pythonEnvironments/base/info/pythonVersion'; +import { + BasicEnvInfo, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, +} from '../../../../../client/pythonEnvironments/base/locator'; +import { PythonEnvsResolver } from '../../../../../client/pythonEnvironments/base/locators/composite/envsResolver'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { + getEnvironmentInfoService, + IEnvironmentInfoService, +} from '../../../../../client/pythonEnvironments/base/info/environmentInfoService'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertEnvEqual, assertEnvsEqual } from '../envTestUtils'; +import { createBasicEnv, getEnvs, getEnvsWithUpdates, SimpleLocator } from '../../common'; +import { getOSType, OSType } from '../../../../common'; +import { CondaInfo } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { createDeferred } from '../../../../../client/common/utils/async'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Python envs locator - Environments Resolver', () => { + let envInfoService: IEnvironmentInfoService; + let disposables: IDisposableRegistry; + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + + setup(() => { + disposables = []; + envInfoService = getEnvironmentInfoService(disposables); + }); + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + /** + * Returns the expected environment to be returned by Environment info service + */ + function createExpectedEnvInfo( + env: PythonEnvInfo, + expectedDisplay: string, + expectedDetailedDisplay: string, + ): PythonEnvInfo { + const updatedEnv = cloneDeep(env); + updatedEnv.version = { + ...parseVersion('3.8.3-final'), + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + }; + updatedEnv.executable.filename = env.executable.filename; + updatedEnv.executable.sysPrefix = 'path'; + updatedEnv.arch = Architecture.x64; + updatedEnv.display = expectedDisplay; + updatedEnv.detailedDisplayName = expectedDetailedDisplay; + updatedEnv.identifiedUsingNativeLocator = updatedEnv.identifiedUsingNativeLocator ?? undefined; + updatedEnv.pythonRunCommand = updatedEnv.pythonRunCommand ?? undefined; + if (env.kind === PythonEnvKind.Conda) { + env.type = PythonEnvType.Conda; + } + return updatedEnv; + } + + function createExpectedResolvedEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + display: string | undefined = undefined, + type?: PythonEnvType, + detailedDisplay?: string, + ): PythonEnvInfo { + return { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display, + detailedDisplayName: detailedDisplay ?? display, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: Uri.file(location), + source: [], + type, + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + } + suite('iterEnvs()', () => { + let stubShellExec: sinon.SinonStub; + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + stubShellExec = sinon.stub(externalDependencies, 'shellExecute'); + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Iterator yields environments after resolving basic envs received from parent iterator', async () => { + const env1 = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const resolvedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + "Python ('win1')", + PythonEnvType.Virtual, + "Python ('win1': venv)", + ); + const envsReturnedByParentLocator = [env1]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const iterator = resolver.iterEnvs(); + const envs = await getEnvs(iterator); + + assertEnvsEqual(envs, [resolvedEnvReturnedByBasicResolver]); + }); + + test('Updates for environments are sent correctly followed by the null event', async () => { + // Arrange + const env1 = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const resolvedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, + ); + const envsReturnedByParentLocator = [env1]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const iterator = resolver.iterEnvs(); + const envs = await getEnvsWithUpdates(iterator); + + assertEnvsEqual(envs, [ + createExpectedEnvInfo( + resolvedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), + ]); + }); + + test('If fetching interpreter info fails, it is not reported in the final list of envs', async () => { + // Arrange + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: '', + }); + }), + ); + // Arrange + const env1 = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const envsReturnedByParentLocator = [env1]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + // Act + const iterator = resolver.iterEnvs(); + const envs = await getEnvsWithUpdates(iterator); + + // Assert + assertEnvsEqual(envs, []); + }); + + test('Updates to environments from the incoming iterator are applied properly', async () => { + // Arrange + const env = createBasicEnv( + PythonEnvKind.Unknown, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const updatedEnv = createBasicEnv( + PythonEnvKind.VirtualEnv, // Ensure this type is discarded. + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const resolvedUpdatedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, + ); + const envsReturnedByParentLocator = [env]; + const didUpdate = new EventEmitter<PythonEnvUpdatedEvent<BasicEnvInfo> | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator, { + onUpdated: didUpdate.event, + }); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + // Act + const iterator = resolver.iterEnvs(); + const iteratorUpdateCallback = () => { + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + didUpdate.fire({ index: 0, old: env, update: updatedEnv }); + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); // It is essential for the incoming iterator to fire event signifying it's done + }; + const envs = await getEnvsWithUpdates(iterator, iteratorUpdateCallback); + + // Assert + assertEnvsEqual(envs, [ + createExpectedEnvInfo( + resolvedUpdatedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), + ]); + didUpdate.dispose(); + }); + + test('Ensure progress updates are emitted correctly', async () => { + // Arrange + const shellExecDeferred = createDeferred<void>(); + stubShellExec.reset(); + stubShellExec.returns( + shellExecDeferred.promise.then( + () => + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ), + ); + const env = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const updatedEnv = createBasicEnv( + PythonEnvKind.Poetry, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const envsReturnedByParentLocator = [env]; + const didUpdate = new EventEmitter<PythonEnvUpdatedEvent<BasicEnvInfo> | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator, { + onUpdated: didUpdate.event, + }); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const iterator = resolver.iterEnvs(); + let stage: ProgressReportStage | undefined; + let waitForProgressEvent = createDeferred<void>(); + iterator.onUpdated!(async (event) => { + if (isProgressEvent(event)) { + stage = event.stage; + waitForProgressEvent.resolve(); + } + }); + // Act + let result = await iterator.next(); + while (!result.done) { + result = await iterator.next(); + } + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryStarted); + + // Act + waitForProgressEvent = createDeferred<void>(); + didUpdate.fire({ index: 0, old: env, update: updatedEnv }); + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.allPathsDiscovered); + + // Act + waitForProgressEvent = createDeferred<void>(); + shellExecDeferred.resolve(); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryFinished); + didUpdate.dispose(); + }); + }); + + test('onChanged fires iff onChanged from resolver fires', () => { + const parentLocator = new SimpleLocator([]); + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; + const expected = [event1, event2]; + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const events: PythonEnvsChangedEvent[] = []; + resolver.onChanged((e) => events.push(e)); + + parentLocator.fire(event1); + parentLocator.fire(event2); + + assert.deepEqual(events, expected); + }); + + suite('resolveEnv()', () => { + let stubShellExec: sinon.SinonStub; + const envsWithoutPython = path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython'); + function condaInfo(condaPrefix: string): CondaInfo { + return { + conda_version: '4.8.0', + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + root_prefix: '/some/prefix', + envs: [condaPrefix], + envs_dirs: [path.dirname(condaPrefix)], + }; + } + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + stubShellExec = sinon.stub(externalDependencies, 'shellExecute'); + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Calls into basic resolver to get environment info, then calls environnment service to resolve environment further and return it', async function () { + if (getOSType() !== OSType.Windows) { + this.skip(); + } + const resolvedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, + ); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')); + + assertEnvEqual( + expected, + createExpectedEnvInfo( + resolvedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), + ); + }); + + test('Resolver should return empty version info for envs lacking an interpreter', async function () { + if (getOSType() !== OSType.Windows) { + this.skip(); + } + sinon.stub(externalDependencies, 'getPythonSetting').withArgs('condaPath').returns('conda'); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { + return { stdout: JSON.stringify(condaInfo(path.join(envsWithoutPython, 'condaLackingPython'))) }; + } + throw new Error(`${command} is missing or is not executable`); + }); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(envsWithoutPython, 'condaLackingPython')); + + assert.deepEqual(expected?.version, getEmptyVersion()); + assert.equal(expected?.display, "Python ('condaLackingPython')"); + assert.equal(expected?.detailedDisplayName, "Python ('condaLackingPython': conda)"); + }); + + test('If running interpreter info throws error, return undefined', async () => { + stubShellExec.returns( + new Promise<ExecutionResult<string>>((_resolve, reject) => { + reject(); + }), + ); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')); + + assert.deepEqual(expected, undefined); + }); + + test('If parsing interpreter info fails, return undefined', async () => { + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stderr: 'Kaboom', + stdout: '', + }); + }), + ); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')); + + assert.deepEqual(expected, undefined); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts new file mode 100644 index 000000000000..22b2f0c01304 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts @@ -0,0 +1,661 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as winreg from '../../../../../client/pythonEnvironments/common/windowsRegistry'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformApis from '../../../../../client/common/utils/platform'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonEnvType, + PythonVersion, + UNKNOWN_PYTHON_VERSION, +} from '../../../../../client/pythonEnvironments/base/info'; +import { buildEnvInfo, setEnvDisplayString } from '../../../../../client/pythonEnvironments/base/info/env'; +import { InterpreterInformation } from '../../../../../client/pythonEnvironments/base/info/interpreter'; +import { parseVersion } from '../../../../../client/pythonEnvironments/base/info/pythonVersion'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertEnvEqual } from '../envTestUtils'; +import { Architecture } from '../../../../../client/common/utils/platform'; +import { + AnacondaCompanyName, + CondaInfo, +} from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { resolveBasicEnv } from '../../../../../client/pythonEnvironments/base/locators/composite/resolverUtils'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Resolver Utils', () => { + let getWorkspaceFolders: sinon.SinonStub; + setup(() => { + sinon.stub(externalDependencies, 'getPythonSetting').withArgs('condaPath').returns('conda'); + getWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolderPaths'); + getWorkspaceFolders.returns([]); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Pyenv', () => { + const testPyenvRoot = path.join(TEST_LAYOUT_ROOT, 'pyenvhome', '.pyenv'); + const testPyenvVersionsDir = path.join(testPyenvRoot, 'versions'); + setup(() => { + sinon.stub(platformApis, 'getEnvironmentVariable').withArgs('PYENV_ROOT').returns(testPyenvRoot); + }); + + teardown(() => { + sinon.restore(); + }); + function getExpectedPyenvInfo1(): PythonEnvInfo | undefined { + const envInfo = buildEnvInfo({ + kind: PythonEnvKind.Pyenv, + executable: path.join(testPyenvVersionsDir, '3.9.0', 'bin', 'python'), + version: { + major: 3, + minor: 9, + micro: 0, + }, + source: [], + }); + envInfo.location = path.join(testPyenvVersionsDir, '3.9.0'); + envInfo.name = '3.9.0'; + setEnvDisplayString(envInfo); + return envInfo; + } + + function getExpectedPyenvInfo2(): PythonEnvInfo | undefined { + const envInfo = buildEnvInfo({ + kind: PythonEnvKind.Pyenv, + executable: path.join(testPyenvVersionsDir, 'miniconda3-4.7.12', 'bin', 'python'), + version: { + major: 3, + minor: 7, + micro: -1, + }, + source: [], + org: 'miniconda3', + type: PythonEnvType.Conda, + }); + envInfo.location = path.join(testPyenvVersionsDir, 'miniconda3-4.7.12'); + envInfo.name = 'base'; + setEnvDisplayString(envInfo); + return envInfo; + } + + test('resolveEnv', async () => { + const executablePath = path.join(testPyenvVersionsDir, '3.9.0', 'bin', 'python'); + const expected = getExpectedPyenvInfo1(); + + const actual = await resolveBasicEnv({ executablePath, kind: PythonEnvKind.Pyenv }); + assertEnvEqual(actual, expected); + }); + + test('resolveEnv (base conda env)', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + const executablePath = path.join(testPyenvVersionsDir, 'miniconda3-4.7.12', 'bin', 'python'); + const expected = getExpectedPyenvInfo2(); + + const actual = await resolveBasicEnv({ executablePath, kind: PythonEnvKind.Pyenv }); + assertEnvEqual(actual, expected); + }); + }); + + suite('Microsoft store', () => { + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + sinon.stub(platformApis, 'getEnvironmentVariable').withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + sinon.restore(); + }); + + function createExpectedInterpreterInfo( + executable: string, + sysVersion?: string, + sysPrefix?: string, + versionStr?: string, + ): InterpreterInformation { + let version: PythonVersion; + try { + version = parseVersion(versionStr ?? path.basename(executable)); + if (sysVersion) { + version.sysVersion = sysVersion; + } + } catch (e) { + version = UNKNOWN_PYTHON_VERSION; + } + return { + version, + arch: Architecture.x64, + executable: { + filename: executable, + sysPrefix: sysPrefix ?? '', + ctime: -1, + mtime: -1, + }, + }; + } + + test('resolveEnv', async () => { + const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); + const expected: PythonEnvInfo = { + display: undefined, + searchLocation: undefined, + name: '', + location: '', + kind: PythonEnvKind.MicrosoftStore, + distro: { org: 'Microsoft' }, + source: [PythonEnvSource.PathEnvVar], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + ...createExpectedInterpreterInfo(python38path), + }; + setEnvDisplayString(expected); + + const actual = await resolveBasicEnv({ + executablePath: python38path, + kind: PythonEnvKind.MicrosoftStore, + }); + + assertEnvEqual(actual, expected); + }); + + test('resolveEnv(string): forbidden path', async () => { + const python38path = path.join(testLocalAppData, 'Program Files', 'WindowsApps', 'python3.8.exe'); + const expected: PythonEnvInfo = { + display: undefined, + searchLocation: undefined, + name: '', + location: '', + kind: PythonEnvKind.MicrosoftStore, + distro: { org: 'Microsoft' }, + source: [PythonEnvSource.PathEnvVar], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + ...createExpectedInterpreterInfo(python38path), + }; + setEnvDisplayString(expected); + + const actual = await resolveBasicEnv({ + executablePath: python38path, + kind: PythonEnvKind.MicrosoftStore, + }); + + assertEnvEqual(actual, expected); + }); + }); + + suite('Conda', () => { + const condaPrefixNonWindows = path.join(TEST_LAYOUT_ROOT, 'conda2'); + const condaPrefixWindows = path.join(TEST_LAYOUT_ROOT, 'conda1'); + const condaInfo: CondaInfo = { + conda_version: '4.8.0', + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + root_prefix: path.dirname(TEST_LAYOUT_ROOT), + envs: [], + envs_dirs: [TEST_LAYOUT_ROOT], + }; + + function expectedEnvInfo(executable: string, location: string, name: string) { + const info = buildEnvInfo({ + executable, + kind: PythonEnvKind.Conda, + org: AnacondaCompanyName, + location, + source: [], + version: UNKNOWN_PYTHON_VERSION, + fileInfo: undefined, + name, + type: PythonEnvType.Conda, + }); + setEnvDisplayString(info); + return info; + } + function createSimpleEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + ): PythonEnvInfo { + const info: PythonEnvInfo = { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display: undefined, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: undefined, + source: [], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + info.type = PythonEnvType.Conda; + setEnvDisplayString(info); + return info; + } + + teardown(() => { + sinon.restore(); + }); + + test('resolveEnv (Windows)', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { + return { stdout: JSON.stringify(condaInfo) }; + } + throw new Error(`${command} is missing or is not executable`); + }); + const actual = await resolveBasicEnv({ + executablePath: path.join(condaPrefixWindows, 'python.exe'), + envPath: condaPrefixWindows, + kind: PythonEnvKind.Conda, + }); + assertEnvEqual( + actual, + expectedEnvInfo( + path.join(condaPrefixWindows, 'python.exe'), + condaPrefixWindows, + path.basename(condaPrefixWindows), + ), + ); + }); + + test('resolveEnv (non-Windows)', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { + return { stdout: JSON.stringify(condaInfo) }; + } + throw new Error(`${command} is missing or is not executable`); + }); + const actual = await resolveBasicEnv({ + executablePath: path.join(condaPrefixNonWindows, 'bin', 'python'), + kind: PythonEnvKind.Conda, + envPath: condaPrefixNonWindows, + }); + assertEnvEqual( + actual, + expectedEnvInfo( + path.join(condaPrefixNonWindows, 'bin', 'python'), + condaPrefixNonWindows, + path.basename(condaPrefixNonWindows), + ), + ); + }); + + test('resolveEnv: If no conda binary found, resolve as an unknown environment', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string) => { + throw new Error(`${command} is missing or is not executable`); + }); + const actual = await resolveBasicEnv({ + executablePath: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), + kind: PythonEnvKind.Conda, + }); + assertEnvEqual( + actual, + createSimpleEnvInfo( + path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), + PythonEnvKind.Unknown, + undefined, + '', + path.join(TEST_LAYOUT_ROOT, 'conda1'), + ), + ); + }); + }); + + suite('Simple envs', () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + setup(() => { + getWorkspaceFolders.returns([testVirtualHomeDir]); + }); + + teardown(() => { + sinon.restore(); + }); + + function createExpectedEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + ): PythonEnvInfo { + const info: PythonEnvInfo = { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display: undefined, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: Uri.file(location), + source: [], + type: PythonEnvType.Virtual, + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + setEnvDisplayString(info); + return info; + } + + test('resolveEnv', async () => { + const expected = createExpectedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + ); + const actual = await resolveBasicEnv({ + executablePath: path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + kind: PythonEnvKind.Venv, + }); + assertEnvEqual(actual, expected); + }); + }); + + suite('Globally-installed envs', () => { + const testPosixKnownPathsRoot = path.join(TEST_LAYOUT_ROOT, 'posixroot'); + const testLocation3 = path.join(testPosixKnownPathsRoot, 'location3'); + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + }); + + teardown(() => { + sinon.restore(); + }); + + function createExpectedEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + ): PythonEnvInfo { + const info: PythonEnvInfo = { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display: undefined, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: undefined, + source: [], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + setEnvDisplayString(info); + return info; + } + + test('resolveEnv', async () => { + const executable = path.join(testLocation3, 'python3.8'); + const expected = createExpectedEnvInfo(executable, PythonEnvKind.OtherGlobal, parseVersion('3.8')); + const actual = await resolveBasicEnv({ + executablePath: executable, + kind: PythonEnvKind.OtherGlobal, + }); + assertEnvEqual(actual, expected); + }); + }); + + suite('Windows registry', () => { + const regTestRoot = path.join(TEST_LAYOUT_ROOT, 'winreg'); + + const registryData = { + x64: { + HKLM: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore', '\\SOFTWARE\\Python\\ContinuumAnalytics'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9', + values: { + '': '', + DisplayName: 'Python 3.9 (64-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '64bit', + SysVersion: '3.9', + Version: '3.9.0rc2', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'py39', 'python.exe'), + }, + subKeys: [] as string[], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics', + values: { + '': '', + }, + subKeys: ['\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64'], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64', + values: { + '': '', + DisplayName: 'Anaconda py38_4.8.3', + SupportUrl: 'github.com/continuumio/anaconda-issues', + SysArchitecture: '64bit', + SysVersion: '3.8', + Version: 'py38_4.8.3', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'conda3', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + HKCU: [], + }, + x86: { + HKLM: [], + HKCU: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8', + values: { + '': '', + DisplayName: 'Python 3.8 (32-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '32bit', + SysVersion: '3.8.5', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'python38', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + }, + }; + + function fakeRegistryValues({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryValue[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const values: winreg.IRegistryValue[] = []; + for (const [name, value] of Object.entries(k.values)) { + values.push({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: k.key, + name, + type: winreg.REG_SZ, + value: value ?? '', + }); + } + return Promise.resolve(values); + } + } + return Promise.resolve([]); + } + + function fakeRegistryKeys({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryKey[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const keys = k.subKeys.map((s) => ({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: s, + })); + return Promise.resolve(keys); + } + } + return Promise.resolve([]); + } + + setup(async () => { + sinon.stub(winreg, 'readRegistryValues').callsFake(fakeRegistryValues); + sinon.stub(winreg, 'readRegistryKeys').callsFake(fakeRegistryKeys); + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + }); + + teardown(() => { + sinon.restore(); + }); + + test('If data provided by registry is more informative than kind resolvers, use it to update environment (64bit)', async () => { + const interpreterPath = path.join(regTestRoot, 'py39', 'python.exe'); + const actual = await resolveBasicEnv({ + executablePath: interpreterPath, + kind: PythonEnvKind.Unknown, + source: [PythonEnvSource.WindowsRegistry], + }); + const expected = buildEnvInfo({ + kind: PythonEnvKind.OtherGlobal, // Environment should be marked as "Global" instead of "Unknown". + executable: interpreterPath, + version: parseVersion('3.9.0rc2'), // Registry provides more complete version info. + arch: Architecture.x64, + org: 'PythonCore', + source: [PythonEnvSource.WindowsRegistry], + }); + setEnvDisplayString(expected); + expected.distro.defaultDisplayName = 'Python 3.9 (64-bit)'; + assertEnvEqual(actual, expected); + }); + + test('If data provided by registry is more informative than kind resolvers, use it to update environment (32bit)', async () => { + const interpreterPath = path.join(regTestRoot, 'python38', 'python.exe'); + const actual = await resolveBasicEnv({ + executablePath: interpreterPath, + kind: PythonEnvKind.Unknown, + source: [PythonEnvSource.WindowsRegistry, PythonEnvSource.PathEnvVar], + }); + const expected = buildEnvInfo({ + kind: PythonEnvKind.OtherGlobal, // Environment should be marked as "Global" instead of "Unknown". + executable: interpreterPath, + version: parseVersion('3.8.5'), // Registry provides more complete version info. + arch: Architecture.x86, // Provided by registry + org: 'PythonCodingPack', // Provided by registry + source: [PythonEnvSource.WindowsRegistry, PythonEnvSource.PathEnvVar], + }); + setEnvDisplayString(expected); + expected.distro.defaultDisplayName = 'Python 3.8 (32-bit)'; + assertEnvEqual(actual, expected); + }); + + test('If data provided by registry is less informative than kind resolvers, do not use it to update environment', async () => { + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string) => { + throw new Error(`${command} is missing or is not executable`); + }); + const interpreterPath = path.join(regTestRoot, 'conda3', 'python.exe'); + const actual = await resolveBasicEnv({ + executablePath: interpreterPath, + kind: PythonEnvKind.Conda, + source: [PythonEnvSource.WindowsRegistry], + }); + const expected = buildEnvInfo({ + location: path.join(regTestRoot, 'conda3'), + // Environment is not marked as Conda, update it to Global. + kind: PythonEnvKind.OtherGlobal, + executable: interpreterPath, + // Registry does not provide the minor version, so keep version provided by Conda resolver instead. + version: parseVersion('3.8.5'), + arch: Architecture.x64, // Provided by registry + org: 'ContinuumAnalytics', // Provided by registry + name: '', + source: [PythonEnvSource.WindowsRegistry], + type: PythonEnvType.Conda, + }); + setEnvDisplayString(expected); + expected.distro.defaultDisplayName = 'Anaconda py38_4.8.3'; + assertEnvEqual(actual, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/envTestUtils.ts b/src/test/pythonEnvironments/base/locators/envTestUtils.ts new file mode 100644 index 000000000000..db29575d29ba --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/envTestUtils.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { exec } from 'child_process'; +import { cloneDeep, zip } from 'lodash'; +import { promisify } from 'util'; +import * as fsapi from '../../../../client/common/platform/fs-paths'; +import { PythonEnvInfo, PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../../../client/pythonEnvironments/base/info'; +import { getEmptyVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; +import { BasicEnvInfo } from '../../../../client/pythonEnvironments/base/locator'; + +const execAsync = promisify(exec); +export async function run(argv: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv }): Promise<void> { + const cmdline = argv.join(' '); + const { stderr } = await execAsync(cmdline, options ?? {}); + if (stderr && stderr.length > 0) { + throw Error(stderr); + } +} + +function normalizeVersion(version: PythonVersion): PythonVersion { + if (version === UNKNOWN_PYTHON_VERSION) { + version = getEmptyVersion(); + } + // Force `undefined` to be set if nothing set. + // eslint-disable-next-line no-self-assign + version.release = version.release; + // eslint-disable-next-line no-self-assign + version.sysVersion = version.sysVersion; + return version; +} + +export function assertVersionsEqual(actual: PythonVersion | undefined, expected: PythonVersion | undefined): void { + if (actual) { + actual = normalizeVersion(actual); + } + if (expected) { + expected = normalizeVersion(expected); + } + assert.deepStrictEqual(actual, expected); +} + +export async function createFile(filename: string, text = ''): Promise<string> { + await fsapi.writeFile(filename, text); + return filename; +} + +export async function deleteFile(filename: string): Promise<void> { + await fsapi.remove(filename); +} + +export function assertEnvEqual(actual: PythonEnvInfo | undefined, expected: PythonEnvInfo | undefined): void { + assert.notStrictEqual(actual, undefined); + assert.notStrictEqual(expected, undefined); + + if (actual) { + // Make sure to clone so we do not alter the original object + actual = cloneDeep(actual); + expected = cloneDeep(expected); + // No need to match these, so reset them + actual.executable.ctime = -1; + actual.executable.mtime = -1; + actual.version = normalizeVersion(actual.version); + if (expected) { + expected.executable.ctime = -1; + expected.executable.mtime = -1; + expected.version = normalizeVersion(expected.version); + delete expected.id; + } + delete actual.id; + + assert.deepStrictEqual(actual, expected); + } +} + +export function assertEnvsEqual( + actualEnvs: (PythonEnvInfo | undefined)[], + expectedEnvs: (PythonEnvInfo | undefined)[], +): void { + actualEnvs = actualEnvs.sort((a, b) => (a && b ? a.executable.filename.localeCompare(b.executable.filename) : 0)); + expectedEnvs = expectedEnvs.sort((a, b) => + a && b ? a.executable.filename.localeCompare(b.executable.filename) : 0, + ); + assert.deepStrictEqual(actualEnvs.length, expectedEnvs.length, 'Number of envs'); + zip(actualEnvs, expectedEnvs).forEach((value) => { + const [actual, expected] = value; + actual?.source.sort(); + expected?.source.sort(); + assertEnvEqual(actual, expected); + }); +} + +export function assertBasicEnvsEqual(actualEnvs: BasicEnvInfo[], expectedEnvs: BasicEnvInfo[]): void { + actualEnvs = actualEnvs + .sort((a, b) => a.executablePath.localeCompare(b.executablePath)) + .map((c) => ({ ...c, executablePath: c.executablePath.toLowerCase() })); + expectedEnvs = expectedEnvs + .sort((a, b) => a.executablePath.localeCompare(b.executablePath)) + .map((c) => ({ ...c, executablePath: c.executablePath.toLowerCase() })); + assert.deepStrictEqual(actualEnvs.length, expectedEnvs.length, 'Number of envs'); + zip(actualEnvs, expectedEnvs).forEach((value) => { + const [actual, expected] = value; + if (actual) { + actual.source = actual.source ?? []; + actual.searchLocation = actual.searchLocation ?? undefined; + actual.source.sort(); + } + if (expected) { + expected.source = expected.source ?? []; + expected.searchLocation = expected.searchLocation ?? undefined; + expected.source.sort(); + } + assert.deepStrictEqual(actual, expected); + }); +} diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts new file mode 100644 index 000000000000..b0b18fb3827e --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsapi from '../../../../../client/common/platform/fs-paths'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { ActiveStateLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/activeStateLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import { createBasicEnv } from '../../common'; +import * as platform from '../../../../../client/common/utils/platform'; +import { ActiveState } from '../../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { replaceAll } from '../../../../../client/common/stringUtils'; + +suite('ActiveState Locator', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + let locator: ActiveStateLocator; + + setup(() => { + locator = new ActiveStateLocator(); + + let homeDir: string; + switch (platform.getOSType()) { + case platform.OSType.Windows: + homeDir = 'C:\\Users\\user'; + break; + case platform.OSType.OSX: + homeDir = '/Users/user'; + break; + default: + homeDir = '/home/user'; + } + sinon.stub(platform, 'getUserHomeDir').returns(homeDir); + + const stateToolDir = ActiveState.getStateToolDir(); + if (stateToolDir) { + sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => Promise.resolve(dir === stateToolDir)); + } + + sinon.stub(externalDependencies, 'getPythonSetting').returns(undefined); + + sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { + if (command === 'state projects -o editor') { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${replaceAll( + path.join(testActiveStateDir, 'c09080d1', 'exec'), + '\\', + '\\\\', + )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${replaceAll( + path.join(testActiveStateDir, '2af6390a', 'exec'), + '\\', + '\\\\', + )}"]}]\n\0`, + }); + } + return Promise.reject(new Error('Command failed')); + }); + }); + + teardown(() => sinon.restore()); + + test('iterEnvs()', async () => { + const actualEnvs = await getEnvs(locator.iterEnvs()); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.ActiveState, + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + platform.getOSType() === platform.OSType.Windows ? 'python3.exe' : 'python3', + ), + ), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..3c7d4348b1c5 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { CondaEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/condaLocator'; +import { sleep } from '../../../../core'; +import { createDeferred, Deferred } from '../../../../../client/common/utils/async'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, TEST_TIMEOUT } from '../../../../constants'; +import { traceWarn } from '../../../../../client/logging'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../../ciConstants'; +import { isCI } from '../../../../../client/common/constants'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; + +class CondaEnvs { + private readonly condaEnvironmentsTxt; + + constructor() { + const home = platformUtils.getUserHomeDir(); + if (!home) { + throw new Error('Home directory not found'); + } + this.condaEnvironmentsTxt = path.join(home, '.conda', 'environments.txt'); + } + + public async create(): Promise<void> { + try { + await fs.createFile(this.condaEnvironmentsTxt); + } catch (err) { + throw new Error(`Failed to create environments.txt ${this.condaEnvironmentsTxt}, Error: ${err}`); + } + } + + public async update(): Promise<void> { + try { + await fs.writeFile(this.condaEnvironmentsTxt, 'path/to/environment'); + } catch (err) { + throw new Error(`Failed to update environments file ${this.condaEnvironmentsTxt}, Error: ${err}`); + } + } + + public async cleanUp() { + try { + await fs.remove(this.condaEnvironmentsTxt); + } catch (err) { + traceWarn(`Failed to clean up ${this.condaEnvironmentsTxt}`); + } + } +} + +suite('Conda Env Locator', async () => { + let locator: CondaEnvironmentLocator; + let condaEnvsTxt: CondaEnvs; + 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'); + + async function waitForChangeToBeDetected(deferred: Deferred<void>) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + let envPaths: any; + + suiteSetup(async () => { + if (isCI) { + envPaths = await fs.readJson(envsLocation); + } + }); + + setup(async () => { + sinon.stub(platformUtils, 'getUserHomeDir').returns(TEST_LAYOUT_ROOT); + condaEnvsTxt = new CondaEnvs(); + await condaEnvsTxt.cleanUp(); + if (isCI) { + sinon.stub(externalDependencies, 'getPythonSetting').returns(envPaths.condaExecPath); + } + }); + + async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { + locator = new CondaEnvironmentLocator(); + // Wait for watchers to get ready + await sleep(1000); + locator.onChanged(onChanged); + } + + teardown(async () => { + await condaEnvsTxt.cleanUp(); + await locator.dispose(); + sinon.restore(); + }); + + test('Fires when conda `environments.txt` file is created', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { providerId: 'conda-envs' }; + await setupLocator(async (e) => { + deferred.resolve(); + actualEvent = e; + }); + + await condaEnvsTxt.create(); + await waitForChangeToBeDetected(deferred); + + assert.deepEqual(actualEvent!, expectedEvent, 'Unexpected event emitted'); + }); + + test('Fires when conda `environments.txt` file is updated', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { providerId: 'conda-envs' }; + await condaEnvsTxt.create(); + await setupLocator(async (e) => { + deferred.resolve(); + actualEvent = e; + }); + + await condaEnvsTxt.update(); + await waitForChangeToBeDetected(deferred); + + assert.deepEqual(actualEvent!, expectedEvent, 'Unexpected event emitted'); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts new file mode 100644 index 000000000000..605109b7a67e --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsapi from '../../../../../client/common/platform/fs-paths'; +import { PythonReleaseLevel, PythonVersion } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDeps from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { getPythonVersionFromConda } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { TEST_DATA_ROOT } from '../../../common/commonTestConstants'; +import { assertVersionsEqual } from '../envTestUtils'; + +suite('Conda Python Version Parser Tests', () => { + let readFileStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + const testDataRoot = path.join(TEST_DATA_ROOT, 'versiondata', 'conda'); + + setup(() => { + readFileStub = sinon.stub(externalDeps, 'readFile'); + sinon.stub(externalDeps, 'inExperiment').returns(false); + + pathExistsStub = sinon.stub(externalDeps, 'pathExists'); + pathExistsStub.resolves(true); + }); + + teardown(() => { + sinon.restore(); + }); + + interface ICondaPythonVersionTestData { + name: string; + historyFileContents: string; + expected: PythonVersion | undefined; + } + + function getTestData(): ICondaPythonVersionTestData[] { + const data: ICondaPythonVersionTestData[] = []; + + const cases = fsapi.readdirSync(testDataRoot).map((c) => path.join(testDataRoot, c)); + const casesToVersion = new Map<string, PythonVersion>(); + casesToVersion.set('case1', { major: 3, minor: 8, micro: 5 }); + + casesToVersion.set('case2', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Alpha, serial: 1 }, + }); + + casesToVersion.set('case3', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Beta, serial: 2 }, + }); + + casesToVersion.set('case4', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Candidate, serial: 1 }, + }); + + casesToVersion.set('case5', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Candidate, serial: 2 }, + }); + + for (const c of cases) { + const name = path.basename(c); + const expected = casesToVersion.get(name); + if (expected) { + data.push({ + name, + historyFileContents: fsapi.readFileSync(c, 'utf-8'), + expected, + }); + } + } + + return data; + } + + const testData = getTestData(); + testData.forEach((data) => { + test(`Parsing ${data.name}`, async () => { + readFileStub.resolves(data.historyFileContents); + + const actual = await getPythonVersionFromConda('/path/here/does/not/matter'); + + assertVersionsEqual(actual, data.expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts new file mode 100644 index 000000000000..e570c3fb72da --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts @@ -0,0 +1,244 @@ +// 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 fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as helpers from '../../../../../client/common/helpers'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { + CustomVirtualEnvironmentLocator, + VENVFOLDERS_SETTING_KEY, + VENVPATH_SETTING_KEY, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('CustomVirtualEnvironment Locator', () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + const testVenvPathWithTilda = path.join('~', 'customfolder'); + let getUserHomeDirStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let locator: CustomVirtualEnvironmentLocator; + let watchLocationForPatternStub: sinon.SinonStub; + let getPythonSettingStub: sinon.SinonStub; + let onDidChangePythonSettingStub: sinon.SinonStub; + let untildify: sinon.SinonStub; + + setup(async () => { + untildify = sinon.stub(helpers, 'untildify'); + untildify.callsFake((value: string) => value.replace('~', testVirtualHomeDir)); + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + getUserHomeDirStub.returns(testVirtualHomeDir); + getPythonSettingStub = sinon.stub(externalDependencies, 'getPythonSetting'); + + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOSTypeStub.returns(platformUtils.OSType.Linux); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + onDidChangePythonSettingStub = sinon.stub(externalDependencies, 'onDidChangePythonSetting'); + onDidChangePythonSettingStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + const expectedDotProjectFile = path.join( + testVirtualHomeDir, + '.local', + 'share', + 'virtualenvs', + 'project2-vnNIWe9P', + '.project', + ); + readFileStub = sinon.stub(externalDependencies, 'readFile'); + readFileStub.withArgs(expectedDotProjectFile).returns(path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2')); + readFileStub.callThrough(); + + locator = new CustomVirtualEnvironmentLocator(); + }); + teardown(async () => { + await locator.dispose(); + sinon.restore(); + }); + + test('iterEnvs(): Windows with both settings set', async () => { + getPythonSettingStub.withArgs('venvPath').returns(testVenvPathWithTilda); + getPythonSettingStub.withArgs('venvFolders').returns(['.venvs', '.virtualenvs', 'Envs']); + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win2', 'bin', 'python.exe'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows with both settings set', async () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + + getPythonSettingStub.withArgs('venvPath').returns(path.join(testWorkspaceFolder, 'posix2conda')); + getPythonSettingStub + .withArgs('venvFolders') + .returns(['.venvs', '.virtualenvs', 'envs', path.join('.local', 'share', 'virtualenvs')]); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix2conda', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), + ), + createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No User home dir set', async () => { + getUserHomeDirStub.returns(undefined); + + getPythonSettingStub.withArgs('venvPath').returns(testVenvPathWithTilda); + getPythonSettingStub.withArgs('venvFolders').returns(['.venvs', '.virtualenvs', 'Envs']); + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win2', 'bin', 'python.exe'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): with only venvFolders set', async () => { + getPythonSettingStub.withArgs('venvFolders').returns(['.venvs', '.virtualenvs', 'Envs']); + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): with only venvPath set', async () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + + getPythonSettingStub.withArgs('venvPath').returns(path.join(testWorkspaceFolder, 'posix2conda')); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix2conda', 'python')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('onChanged fires if venvPath setting changes', async () => { + const events: PythonEnvsChangedEvent[] = []; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; + locator.onChanged((e) => events.push(e)); + + await getEnvs(locator.iterEnvs()); + const venvPathCall = onDidChangePythonSettingStub + .getCalls() + .filter((c) => c.args[0] === VENVPATH_SETTING_KEY)[0]; + const callback = venvPathCall.args[1]; + callback(); // Callback is called when venvPath setting changes + + assert.deepEqual(events, expected, 'Unexpected events'); + }); + + test('onChanged fires if venvFolders setting changes', async () => { + const events: PythonEnvsChangedEvent[] = []; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; + locator.onChanged((e) => events.push(e)); + + await getEnvs(locator.iterEnvs()); + const venvFoldersCall = onDidChangePythonSettingStub + .getCalls() + .filter((c) => c.args[0] === VENVFOLDERS_SETTING_KEY)[0]; + const callback = venvFoldersCall.args[1]; + callback(); // Callback is called when venvFolders setting changes + + assert.deepEqual(events, expected, 'Unexpected events'); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.unit.test.ts new file mode 100644 index 000000000000..fc1c6927d3fe --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.unit.test.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; +import { Disposables } from '../../../../../client/common/utils/resourceLifecycle'; +import { PythonEnvInfo, PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../../../../client/pythonEnvironments/base/locator'; +import { + FSWatcherKind, + FSWatchingLocator, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator'; +import * as binWatcher from '../../../../../client/pythonEnvironments/common/pythonBinariesWatcher'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; + +suite('File System Watching Locator Tests', () => { + const baseDir = TEST_LAYOUT_ROOT; + const fakeDir = '/this/is/a/fake/path'; + const callback = async () => Promise.resolve(PythonEnvKind.System); + let watchLocationStub: sinon.SinonStub; + + setup(() => { + watchLocationStub = sinon.stub(binWatcher, 'watchLocationForPythonBinaries'); + watchLocationStub.resolves(new Disposables()); + }); + + teardown(() => { + sinon.restore(); + }); + + class TestWatcher extends FSWatchingLocator { + public readonly providerId: string = 'test'; + + constructor( + watcherKind: FSWatcherKind, + opts: { + envStructure?: binWatcher.PythonEnvStructure; + } = {}, + ) { + super(() => [baseDir, fakeDir], callback, opts, watcherKind); + } + + public async initialize() { + await this.initWatchers(); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator<BasicEnvInfo> { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + protected doResolveEnv(): Promise<PythonEnvInfo | undefined> { + throw new Error('Method not implemented.'); + } + } + + [ + binWatcher.PythonEnvStructure.Standard, + binWatcher.PythonEnvStructure.Flat, + // `undefined` means "use the default". + undefined, + ].forEach((envStructure) => { + suite(`${envStructure || 'default'} structure`, () => { + const expected = + getOSType() === OSType.Windows + ? [ + // The first one is the basename glob. + 'python.exe', + '*/python.exe', + '*/Scripts/python.exe', + ] + : [ + // The first one is the basename glob. + 'python', + '*/python', + '*/bin/python', + ]; + if (envStructure === binWatcher.PythonEnvStructure.Flat) { + while (expected.length > 1) { + expected.pop(); + } + } + + const watcherKinds = [FSWatcherKind.Global, FSWatcherKind.Workspace]; + + const opts = { + envStructure, + }; + + watcherKinds.forEach((watcherKind) => { + test(`watching ${FSWatcherKind[watcherKind]}`, async () => { + const testWatcher = new TestWatcher(watcherKind, opts); + await testWatcher.initialize(); + + // Watcher should be called for all workspace locators. For global locators it should never be called. + if (watcherKind === FSWatcherKind.Workspace) { + assert.strictEqual(watchLocationStub.callCount, expected.length); + expected.forEach((glob) => { + assert.ok(watchLocationStub.calledWithMatch(baseDir, sinon.match.any, glob)); + assert.strictEqual( + // As directory does not exist, it should not be watched. + watchLocationStub.calledWithMatch(fakeDir, sinon.match.any, glob), + false, + ); + }); + } else if (watcherKind === FSWatcherKind.Global) { + assert.ok(watchLocationStub.notCalled); + } + }); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..eb88b2c48d56 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.testvirtualenvs.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { GlobalVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { testLocatorWatcher } from './watcherTestUtils'; + +suite('GlobalVirtualEnvironment Locator', async () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + const testWorkOnHomePath = path.join(testVirtualHomeDir, 'workonhome'); + let workonHomeOldValue: string | undefined; + suiteSetup(async function () { + // https://github.com/microsoft/vscode-python/issues/17798 + return this.skip(); + workonHomeOldValue = process.env.WORKON_HOME; + process.env.WORKON_HOME = testWorkOnHomePath; + }); + testLocatorWatcher(testWorkOnHomePath, async () => new GlobalVirtualEnvironmentLocator()); + suiteTeardown(() => { + process.env.WORKON_HOME = workonHomeOldValue; + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts new file mode 100644 index 000000000000..ede947073ea2 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { GlobalVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('GlobalVirtualEnvironment Locator', () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + const testWorkOnHomePath = path.join(testVirtualHomeDir, 'workonhome'); + let getEnvVariableStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let locator: GlobalVirtualEnvironmentLocator; + let watchLocationForPatternStub: sinon.SinonStub; + const project2 = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2'); + + setup(async () => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getEnvVariableStub.withArgs('WORKON_HOME').returns(testWorkOnHomePath); + + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + getUserHomeDirStub.returns(testVirtualHomeDir); + + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOSTypeStub.returns(platformUtils.OSType.Linux); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + const expectedDotProjectFile = path.join( + testVirtualHomeDir, + '.local', + 'share', + 'virtualenvs', + 'project2-vnNIWe9P', + '.project', + ); + readFileStub = sinon.stub(externalDependencies, 'readFile'); + readFileStub.withArgs(expectedDotProjectFile).returns(project2); + readFileStub.callThrough(); + }); + teardown(async () => { + await locator.dispose(); + readFileStub.restore(); + getEnvVariableStub.restore(); + getUserHomeDirStub.restore(); + getOSTypeStub.restore(); + watchLocationForPatternStub.restore(); + }); + + test('iterEnvs(): Windows', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'win2', 'bin', 'python.exe'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Windows (WORKON_HOME NOT set)', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows', async () => { + const pipenv = createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ); + pipenv.searchLocation = Uri.file(project2); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), + createBasicEnv(PythonEnvKind.VirtualEnv, path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), + ), + pipenv, + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): with depth set', async () => { + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.VirtualEnv, path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(1); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows (WORKON_HOME not set)', async () => { + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + const pipenv = createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ); + pipenv.searchLocation = Uri.file(project2); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), + ), + pipenv, + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No User home dir set', async () => { + getUserHomeDirStub.returns(undefined); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No default virtual environment dirs ', async () => { + // We can simulate that by pointing the user home dir to some random directory + getUserHomeDirStub.returns(path.join('some', 'random', 'directory')); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(2); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts new file mode 100644 index 000000000000..9a2a69908f2a --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { HatchLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/hatchLocator'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { createBasicEnv } from '../../common'; +import { makeExecHandler, projectDirs, venvDirs } from '../../../common/environmentManagers/hatch.unit.test'; + +suite('Hatch Locator', () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSType: sinon.SinonStub; + let locator: HatchLocator; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('hatch'); + getOSType = sinon.stub(platformUtils, 'getOSType'); + exec = sinon.stub(externalDependencies, 'exec'); + }); + + suiteTeardown(() => sinon.restore()); + + suite('iterEnvs()', () => { + setup(() => { + getOSType.returns(platformUtils.OSType.Linux); + }); + + interface TestArgs { + osType?: platformUtils.OSType; + pythonBin?: string; + } + + const testProj1 = async ({ osType, pythonBin = 'bin/python' }: TestArgs = {}) => { + if (osType) { + getOSType.returns(osType); + } + + locator = new HatchLocator(projectDirs.project1); + exec.callsFake(makeExecHandler(venvDirs.project1, { path: true, cwd: projectDirs.project1 })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = [createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project1.default, pythonBin))]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }; + + test('project with only the default env', () => testProj1()); + test('project with only the default env on Windows', () => + testProj1({ + osType: platformUtils.OSType.Windows, + pythonBin: 'Scripts/python.exe', + })); + + test('project with multiple defined envs', async () => { + locator = new HatchLocator(projectDirs.project2); + exec.callsFake(makeExecHandler(venvDirs.project2, { path: true, cwd: projectDirs.project2 })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.default, 'bin/python')), + createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.test, 'bin/python')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/macDefaultLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/macDefaultLocator.unit.test.ts new file mode 100644 index 000000000000..62339df7e144 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/macDefaultLocator.unit.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as osUtils from '../../../../../client/common/utils/platform'; +import { isMacDefaultPythonPath } from '../../../../../client/pythonEnvironments/common/environmentManagers/macDefault'; + +suite('isMacDefaultPythonPath', () => { + let getOSTypeStub: sinon.SinonStub; + + setup(() => { + getOSTypeStub = sinon.stub(osUtils, 'getOSType'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testCases: { path: string; os: osUtils.OSType; expected: boolean }[] = [ + { path: '/usr/bin/python', os: osUtils.OSType.OSX, expected: true }, + { path: '/usr/bin/python', os: osUtils.OSType.Linux, expected: false }, + { path: '/usr/bin/python2', os: osUtils.OSType.OSX, expected: true }, + { path: '/usr/local/bin/python2', os: osUtils.OSType.OSX, expected: false }, + { path: '/usr/bin/python3', os: osUtils.OSType.OSX, expected: false }, + { path: '/usr/bin/python3', os: osUtils.OSType.Linux, expected: false }, + ]; + + testCases.forEach(({ path, os, expected }) => { + const testName = `If the Python path is ${path} on ${os}, it is${ + expected ? '' : ' not' + } a macOS default Python path`; + + test(testName, () => { + getOSTypeStub.returns(os); + + const result = isMacDefaultPythonPath(path); + + assert.strictEqual(result, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts new file mode 100644 index 000000000000..511597dd28db --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; +import { createDeferred, Deferred, sleep } from '../../../../../client/common/utils/async'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as externalDeps from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { MicrosoftStoreLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator'; +import { TEST_TIMEOUT } from '../../../../constants'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { traceWarn } from '../../../../../client/logging'; + +class MicrosoftStoreEnvs { + private executables: string[] = []; + + private dirs: string[] = []; + + constructor(private readonly storeAppRoot: string) {} + + public async create(version: string): Promise<string> { + const dirName = path.join(this.storeAppRoot, `PythonSoftwareFoundation.Python.${version}_qbz5n2kfra8p0`); + const filename = path.join(this.storeAppRoot, `python${version}.exe`); + try { + await fs.createFile(filename); + } catch (err) { + throw new Error(`Failed to create Windows Apps executable ${filename}, Error: ${err}`); + } + try { + await fs.mkdir(dirName); + } catch (err) { + throw new Error(`Failed to create Windows Apps directory ${dirName}, Error: ${err}`); + } + this.executables.push(filename); + this.dirs.push(dirName); + return filename; + } + + public async update(version: string): Promise<void> { + // On update microsoft store removes the directory and re-adds it. + const dirName = path.join(this.storeAppRoot, `PythonSoftwareFoundation.Python.${version}_qbz5n2kfra8p0`); + try { + await fs.rmdir(dirName); + await fs.mkdir(dirName); + } catch (err) { + throw new Error(`Failed to update Windows Apps directory ${dirName}, Error: ${err}`); + } + } + + public async cleanUp() { + await Promise.all( + this.executables.map(async (filename: string) => { + try { + await fs.remove(filename); + } catch (err) { + traceWarn(`Failed to clean up ${filename}`); + } + }), + ); + await Promise.all( + this.dirs.map(async (dir: string) => { + try { + await fs.rmdir(dir); + } catch (err) { + traceWarn(`Failed to clean up ${dir}`); + } + }), + ); + } +} + +suite('Microsoft Store Locator', async () => { + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + const windowsStoreEnvs = new MicrosoftStoreEnvs(testStoreAppRoot); + let locator: MicrosoftStoreLocator; + + const localAppDataOldValue = process.env.LOCALAPPDATA; + + async function waitForChangeToBeDetected(deferred: Deferred<void>) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + + async function isLocated(executable: string): Promise<boolean> { + const items = await getEnvs(locator.iterEnvs()); + return items.some((item) => externalDeps.arePathsSame(item.executablePath, executable)); + } + + suiteSetup(async function () { + // Enable once this is done: https://github.com/microsoft/vscode-python/issues/17797 + return this.skip(); + process.env.LOCALAPPDATA = testLocalAppData; + await windowsStoreEnvs.cleanUp(); + }); + + async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { + locator = new MicrosoftStoreLocator(); + await getEnvs(locator.iterEnvs()); // Force the watchers to start. + // Wait for watchers to get ready + await sleep(1000); + locator.onChanged(onChanged); + } + + teardown(async () => { + await windowsStoreEnvs.cleanUp(); + await locator.dispose(); + }); + suiteTeardown(async () => { + process.env.LOCALAPPDATA = localAppDataOldValue; + }); + + test('Detect a new environment', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { + kind: PythonEnvKind.MicrosoftStore, + type: FileChangeType.Created, + searchLocation: Uri.file(testStoreAppRoot), + }; + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + const executable = await windowsStoreEnvs.create('3.4'); + await waitForChangeToBeDetected(deferred); + const isFound = await isLocated(executable); + + assert.ok(isFound); + assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); + }); + + test('Detect when an environment has been deleted', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { + kind: PythonEnvKind.MicrosoftStore, + type: FileChangeType.Deleted, + searchLocation: Uri.file(testStoreAppRoot), + }; + const executable = await windowsStoreEnvs.create('3.4'); + // Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. + await sleep(100); + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + await windowsStoreEnvs.cleanUp(); + await waitForChangeToBeDetected(deferred); + const isFound = await isLocated(executable); + + assert.notOk(isFound); + assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); + }); + + test('Detect when an environment has been updated', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { + kind: PythonEnvKind.MicrosoftStore, + type: FileChangeType.Changed, + searchLocation: Uri.file(testStoreAppRoot), + }; + const executable = await windowsStoreEnvs.create('3.4'); + // Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. + await sleep(100); + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + await windowsStoreEnvs.update('3.4'); + await waitForChangeToBeDetected(deferred); + const isFound = await isLocated(executable); + + assert.ok(isFound); + assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.unit.test.ts new file mode 100644 index 000000000000..98d9602e9729 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.unit.test.ts @@ -0,0 +1,133 @@ +// 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 fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import * as platformApis from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo } from '../../../../../client/pythonEnvironments/base/locator'; +import * as externalDep from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { + getMicrosoftStorePythonExes, + MicrosoftStoreLocator, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator'; +import { getEnvs } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('Microsoft Store', () => { + suite('Utils', () => { + let getEnvVarStub: sinon.SinonStub; + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVarStub.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + getEnvVarStub.restore(); + }); + + test('Store Python Interpreters', async () => { + const expected = [ + path.join(testStoreAppRoot, 'python3.7.exe'), + path.join(testStoreAppRoot, 'python3.8.exe'), + ]; + + const actual = await getMicrosoftStorePythonExes(); + assert.deepEqual(actual, expected); + }); + }); + + suite('Locator', () => { + let stubShellExec: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + let locator: MicrosoftStoreLocator; + let watchLocationForPatternStub: sinon.SinonStub; + + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + const pathToData = new Map< + string, + { + versionInfo: (string | number)[]; + sysPrefix: string; + sysVersion: string; + is64Bit: boolean; + } + >(); + + const python383data = { + versionInfo: [3, 8, 3, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + is64Bit: true, + }; + + const python379data = { + versionInfo: [3, 7, 9, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 16:30:00) [MSC v.1900 64 bit (AMD64)]', + is64Bit: true, + }; + + pathToData.set(path.join(testStoreAppRoot, 'python3.8.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.7.exe'), python379data); + + function createExpectedInfo(executable: string): BasicEnvInfo { + return { + executablePath: executable, + kind: PythonEnvKind.MicrosoftStore, + }; + } + + setup(async () => { + stubShellExec = sinon.stub(externalDep, 'shellExecute'); + stubShellExec.callsFake((command: string) => { + if (command.indexOf('notpython.exe') > 0) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + if (command.indexOf('python3.7.exe') > 0) { + return Promise.resolve<ExecutionResult<string>>({ stdout: JSON.stringify(python379data) }); + } + return Promise.resolve<ExecutionResult<string>>({ stdout: JSON.stringify(python383data) }); + }); + + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + locator = new MicrosoftStoreLocator(); + }); + + teardown(async () => { + await locator.dispose(); + sinon.restore(); + }); + + test('iterEnvs()', async () => { + const expectedEnvs = [ + createExpectedInfo(path.join(testStoreAppRoot, 'python3.7.exe')), + createExpectedInfo(path.join(testStoreAppRoot, 'python3.8.exe')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)).sort((a, b) => + a.executablePath.localeCompare(b.executablePath), + ); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts new file mode 100644 index 000000000000..b55f61c3a771 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { PixiLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/pixiLocator'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { makeExecHandler, projectDirs } from '../../../common/environmentManagers/pixi.unit.test'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { createBasicEnv } from '../../common'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('Pixi Locator', () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSType: sinon.SinonStub; + let locator: PixiLocator; + let pathExistsStub: sinon.SinonStub; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('pixi'); + getOSType = sinon.stub(platformUtils, 'getOSType'); + exec = sinon.stub(externalDependencies, 'exec'); + pathExistsStub = sinon.stub(externalDependencies, 'pathExists'); + pathExistsStub.resolves(true); + }); + + suiteTeardown(() => sinon.restore()); + + suite('iterEnvs()', () => { + interface TestArgs { + projectDir: string; + osType: platformUtils.OSType; + pythonBin: string; + } + + const testProject = async ({ projectDir, osType, pythonBin }: TestArgs) => { + getOSType.returns(osType); + + locator = new PixiLocator(projectDir); + exec.callsFake(makeExecHandler({ cwd: projectDir })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const envPath = path.join(projectDir, '.pixi', 'envs', 'default'); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Pixi, path.join(envPath, pythonBin), undefined, envPath), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }; + + test('project with only the default env', () => + testProject({ + projectDir: projectDirs.nonWindows.path, + osType: platformUtils.OSType.Linux, + pythonBin: 'bin/python', + })); + test('project with only the default env on Windows', () => + testProject({ + projectDir: projectDirs.windows.path, + osType: platformUtils.OSType.Windows, + pythonBin: 'python.exe', + })); + + test('project with multiple environments', async () => { + getOSType.returns(platformUtils.OSType.Linux); + + exec.callsFake(makeExecHandler({ cwd: projectDirs.multiEnv.path })); + + locator = new PixiLocator(projectDirs.multiEnv.path); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = projectDirs.multiEnv.info.environments_info.map((info) => + createBasicEnv(PythonEnvKind.Pixi, path.join(info.prefix, 'bin/python'), undefined, info.prefix), + ); + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts new file mode 100644 index 000000000000..e7982a4c4e9a --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PoetryLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/poetryLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { ExecutionResult, ShellOptions } from '../../../../../client/common/process/types'; +import { createBasicEnv as createBasicEnvCommon } from '../../common'; +import { BasicEnvInfo } from '../../../../../client/pythonEnvironments/base/locator'; + +suite('Poetry Locator', () => { + let shellExecute: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + const testPoetryDir = path.join(TEST_LAYOUT_ROOT, 'poetry'); + let locator: PoetryLocator; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('poetry'); + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + }); + + suiteTeardown(() => sinon.restore()); + + suite('Windows', () => { + const project1 = path.join(testPoetryDir, 'project1'); + + function createBasicEnv( + kind: PythonEnvKind, + executablePath: string, + source?: PythonEnvSource[], + envPath?: string, + ): BasicEnvInfo { + const basicEnv = createBasicEnvCommon(kind, executablePath, source, envPath); + basicEnv.searchLocation = Uri.file(project1); + return basicEnv; + } + setup(() => { + locator = new PoetryLocator(project1); + getOSTypeStub.returns(platformUtils.OSType.Windows); + shellExecute.callsFake((command: string, options: ShellOptions) => { + if (command === 'poetry env list --full-path') { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project1)) { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `${path.join(testPoetryDir, 'poetry-tutorial-project-6hnqYwvD-py3.8')} \n + ${path.join(testPoetryDir, 'globalwinproject-9hvDnqYw-py3.11')} (Activated)\r\n + ${path.join(testPoetryDir, 'someRandomPathWhichDoesNotExist')} `, + }); + } + } + return Promise.reject(new Error('Command failed')); + }); + }); + + test('iterEnvs()', async () => { + // Act + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + // Assert + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'poetry-tutorial-project-6hnqYwvD-py3.8', 'Scripts', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'globalwinproject-9hvDnqYw-py3.11', 'Scripts', 'python.exe'), + ), + createBasicEnv(PythonEnvKind.Poetry, path.join(project1, '.venv', 'Scripts', 'python.exe')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); + + suite('Non-Windows', () => { + const project2 = path.join(testPoetryDir, 'project2'); + + function createBasicEnv( + kind: PythonEnvKind, + executablePath: string, + source?: PythonEnvSource[], + envPath?: string, + ): BasicEnvInfo { + const basicEnv = createBasicEnvCommon(kind, executablePath, source, envPath); + basicEnv.searchLocation = Uri.file(project2); + return basicEnv; + } + setup(() => { + locator = new PoetryLocator(project2); + getOSTypeStub.returns(platformUtils.OSType.Linux); + shellExecute.callsFake((command: string, options: ShellOptions) => { + if (command === 'poetry env list --full-path') { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project2)) { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `${path.join(testPoetryDir, 'posix1project-9hvDnqYw-py3.4')} (Activated)\n + ${path.join(testPoetryDir, 'posix2project-6hnqYwvD-py3.7')}`, + }); + } + } + return Promise.reject(new Error('Command failed')); + }); + }); + + test('iterEnvs()', async () => { + // Act + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + // Assert + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'posix1project-9hvDnqYw-py3.4', 'python'), + ), + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'posix2project-6hnqYwvD-py3.7', 'bin', 'python'), + ), + createBasicEnv(PythonEnvKind.Poetry, path.join(project2, '.venv', 'bin', 'python')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.unit.test.ts new file mode 100644 index 000000000000..7a9a2bc6475d --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.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 semver from 'semver'; +import * as executablesAPI from '../../../../../client/common/utils/exec'; +import * as osUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo } from '../../../../../client/pythonEnvironments/base/locator'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PosixKnownPathsLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { isMacDefaultPythonPath } from '../../../../../client/pythonEnvironments/common/environmentManagers/macDefault'; + +suite('Posix Known Path Locator', () => { + let getPathEnvVar: sinon.SinonStub; + let locator: PosixKnownPathsLocator; + + const testPosixKnownPathsRoot = path.join(TEST_LAYOUT_ROOT, 'posixroot'); + + const testLocation1 = path.join(testPosixKnownPathsRoot, 'location1'); + const testLocation2 = path.join(testPosixKnownPathsRoot, 'location2'); + const testLocation3 = path.join(testPosixKnownPathsRoot, 'location3'); + + const testFileData: Map<string, string[]> = new Map(); + + testFileData.set(testLocation1, ['python', 'python3']); + testFileData.set(testLocation2, ['python', 'python37', 'python38']); + testFileData.set(testLocation3, ['python3.7', 'python3.8']); + + setup(async () => { + getPathEnvVar = sinon.stub(executablesAPI, 'getSearchPathEntries'); + locator = new PosixKnownPathsLocator(); + }); + teardown(() => { + sinon.restore(); + }); + + test('iterEnvs(): get python bin from known test roots', async () => { + const testLocations = [testLocation1, testLocation2, testLocation3]; + getPathEnvVar.returns(testLocations); + + const expectedEnvs: BasicEnvInfo[] = []; + testLocations.forEach((location) => { + const binaries = testFileData.get(location); + if (binaries) { + binaries.forEach((binary) => { + expectedEnvs.push({ + source: [PythonEnvSource.PathEnvVar], + ...createBasicEnv(PythonEnvKind.OtherGlobal, path.join(location, binary)), + }); + }); + } + }); + + const actualEnvs = (await getEnvs(locator.iterEnvs())).filter((e) => e.executablePath.indexOf('posixroot') > 0); + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Do not return Python 2 installs when on macOS Monterey', async function () { + if (osUtils.getOSType() !== osUtils.OSType.OSX) { + this.skip(); + } + + const getOSTypeStub = sinon.stub(osUtils, 'getOSType'); + const gteStub = sinon.stub(semver, 'gte'); + + getOSTypeStub.returns(osUtils.OSType.OSX); + gteStub.returns(true); + + const actualEnvs = await getEnvs(locator.iterEnvs()); + + const globalPython2Envs = actualEnvs.filter((env) => isMacDefaultPythonPath(env.executablePath)); + + assert.strictEqual(globalPython2Envs.length, 0); + }); + + test('iterEnvs(): Return Python 2 installs when not on macOS Monterey', async function () { + if (osUtils.getOSType() !== osUtils.OSType.OSX) { + this.skip(); + } + + const getOSTypeStub = sinon.stub(osUtils, 'getOSType'); + const gteStub = sinon.stub(semver, 'gte'); + + getOSTypeStub.returns(osUtils.OSType.OSX); + gteStub.returns(false); + + const actualEnvs = await getEnvs(locator.iterEnvs()); + + const globalPython2Envs = actualEnvs.filter((env) => isMacDefaultPythonPath(env.executablePath)); + + assert.notStrictEqual(globalPython2Envs.length, 0); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..c370a8ff6da5 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.testvirtualenvs.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { PyenvLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/pyenvLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { testLocatorWatcher } from './watcherTestUtils'; + +suite('Pyenv Locator', async () => { + const testPyenvRoot = path.join(TEST_LAYOUT_ROOT, 'pyenvhome', '.pyenv'); + const testPyenvVersionsDir = path.join(testPyenvRoot, 'versions'); + let pyenvRootOldValue: string | undefined; + suiteSetup(async function () { + // https://github.com/microsoft/vscode-python/issues/17798 + return this.skip(); + pyenvRootOldValue = process.env.PYENV_ROOT; + process.env.PYENV_ROOT = testPyenvRoot; + }); + testLocatorWatcher(testPyenvVersionsDir, async () => new PyenvLocator(), { kind: PythonEnvKind.Pyenv }); + suiteTeardown(() => { + process.env.PYENV_ROOT = pyenvRootOldValue; + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.unit.test.ts new file mode 100644 index 000000000000..19f8088db65f --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PyenvLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/pyenvLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('Pyenv Locator Tests', () => { + let getEnvVariableStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let locator: PyenvLocator; + let watchLocationForPatternStub: sinon.SinonStub; + + const testPyenvRoot = path.join(TEST_LAYOUT_ROOT, 'pyenvhome', '.pyenv'); + const testPyenvVersionsDir = path.join(testPyenvRoot, 'versions'); + + setup(async () => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getEnvVariableStub.withArgs('PYENV_ROOT').returns(testPyenvRoot); + + getOsTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOsTypeStub.returns(platformUtils.OSType.Linux); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + locator = new PyenvLocator(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('iterEnvs()', async () => { + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, '3.9.0', 'bin', 'python')), + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, 'conda1', 'bin', 'python')), + + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, 'miniconda3-4.7.12', 'bin', 'python')), + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, 'venv1', 'bin', 'python')), + ]; + + const actualEnvs = await getEnvs(locator.iterEnvs()); + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts new file mode 100644 index 000000000000..e9c7be3ec321 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; +import { IDisposable } from '../../../../../client/common/types'; +import { createDeferred, Deferred, sleep } from '../../../../../client/common/utils/async'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; +import { traceWarn } from '../../../../../client/logging'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo, ILocator } from '../../../../../client/pythonEnvironments/base/locator'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import { getInterpreterPathFromDir } from '../../../../../client/pythonEnvironments/common/commonUtils'; +import * as externalDeps from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { deleteFiles, PYTHON_PATH } from '../../../../common'; +import { TEST_TIMEOUT } from '../../../../constants'; +import { run } from '../envTestUtils'; + +/** + * A utility class used to create, delete, or modify environments. Primarily used for watcher + * tests, where we need to create environments. + */ +class Venvs { + constructor(private readonly root: string, private readonly prefix = '.virtualenv-') {} + + public async create(name: string): Promise<{ executable: string; envDir: string }> { + const envName = this.resolve(name); + const argv = [PYTHON_PATH.fileToCommandArgumentForPythonExt(), '-m', 'virtualenv', envName]; + try { + await run(argv, { cwd: this.root }); + } catch (err) { + throw new Error(`Failed to create Env ${path.basename(envName)} Error: ${err}`); + } + const dirToLookInto = path.join(this.root, envName); + const filename = await getInterpreterPathFromDir(dirToLookInto); + if (!filename) { + throw new Error(`No environment to update exists in ${dirToLookInto}`); + } + return { executable: filename, envDir: path.dirname(path.dirname(filename)) }; + } + + /** + * Creates a dummy environment by creating a fake executable. + * @param name environment suffix name to create + */ + public async createDummyEnv( + name: string, + kind: PythonEnvKind | undefined, + ): Promise<{ executable: string; envDir: string }> { + const envName = this.resolve(name); + const interpreterPath = path.join(this.root, envName, getOSType() === OSType.Windows ? 'python.exe' : 'python'); + const configPath = path.join(this.root, envName, 'pyvenv.cfg'); + try { + await fs.createFile(interpreterPath); + if (kind === PythonEnvKind.Venv) { + await fs.createFile(configPath); + await fs.writeFile(configPath, 'version = 3.9.2'); + } + } catch (err) { + throw new Error(`Failed to create python executable ${interpreterPath}, Error: ${err}`); + } + return { executable: interpreterPath, envDir: path.dirname(interpreterPath) }; + } + + // eslint-disable-next-line class-methods-use-this + public async update(filename: string): Promise<void> { + try { + await fs.writeFile(filename, 'Environment has been updated'); + } catch (err) { + throw new Error(`Failed to update Workspace virtualenv executable ${filename}, Error: ${err}`); + } + } + + // eslint-disable-next-line class-methods-use-this + public async delete(filename: string): Promise<void> { + try { + await fs.remove(filename); + } catch (err) { + traceWarn(`Failed to clean up ${filename}`); + } + } + + public async cleanUp(): Promise<void> { + const globPattern = path.join(this.root, `${this.prefix}*`); + await deleteFiles(globPattern); + } + + private resolve(name: string): string { + // Ensure env is random to avoid conflicts in tests (corrupting test data) + const now = new Date().getTime().toString().substr(-8); + return `${this.prefix}${name}${now}`; + } +} + +type locatorFactoryFuncType1 = () => Promise<ILocator<BasicEnvInfo> & IDisposable>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type locatorFactoryFuncType2 = (_: any) => Promise<ILocator<BasicEnvInfo> & IDisposable>; + +export type locatorFactoryFuncType = locatorFactoryFuncType1 & locatorFactoryFuncType2; + +/** + * Test if we're able to: + * * Detect a new environment + * * Detect when an environment has been deleted + * * Detect when an environment has been updated + * @param root The root folder where we create, delete, or modify environments. + * @param createLocatorFactoryFunc The factory function used to create the locator. + */ +export function testLocatorWatcher( + root: string, + createLocatorFactoryFunc: locatorFactoryFuncType, + options?: { + /** + * Argument to the locator factory function if any. + */ + arg?: string; + /** + * Environment kind to check for in watcher events. + * If not specified the check is skipped is default. This is because detecting kind of virtual env + * often depends on the file structure around the executable, so we need to wait before attempting + * to verify it. Omitting that check in those cases as we can never deterministically say when it's + * ready to check. + */ + kind?: PythonEnvKind; + /** + * For search based locators it is possible to verify if the environment is now being located, as it + * can be searched for. But for non-search based locators, for eg. which rely on running commands to + * get environments, it's not possible to verify it without executing actual commands, installing tools + * etc, so this option is useful for those locators. + */ + doNotVerifyIfLocated?: boolean; + }, +): void { + let locator: ILocator<BasicEnvInfo> & IDisposable; + const venvs = new Venvs(root); + + async function waitForChangeToBeDetected(deferred: Deferred<void>) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + + async function isLocated(executable: string): Promise<boolean> { + const items = await getEnvs(locator.iterEnvs()); + return items.some((item) => externalDeps.arePathsSame(item.executablePath, executable)); + } + + suiteSetup(async function () { + if (getOSType() === OSType.Linux) { + this.skip(); + } + await venvs.cleanUp(); + }); + async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { + locator = options?.arg ? await createLocatorFactoryFunc(options.arg) : await createLocatorFactoryFunc(); + locator.onChanged(onChanged); + await getEnvs(locator.iterEnvs()); // Force the FS watcher to start. + // Wait for watchers to get ready + await sleep(2000); + } + + teardown(async () => { + if (locator) { + await locator.dispose(); + } + await venvs.cleanUp(); + }); + + test('Detect a new environment', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + const { executable, envDir } = await venvs.create('one'); + await waitForChangeToBeDetected(deferred); + if (!options?.doNotVerifyIfLocated) { + const isFound = await isLocated(executable); + assert.ok(isFound); + } + + assert.strictEqual(actualEvent!.type, FileChangeType.Created, 'Wrong event emitted'); + if (options?.kind) { + assert.strictEqual(actualEvent!.kind, options.kind, 'Wrong event emitted'); + } + assert.notStrictEqual(actualEvent!.searchLocation, undefined, 'Wrong event emitted'); + assert.ok( + externalDeps.arePathsSame(actualEvent!.searchLocation!.fsPath, path.dirname(envDir)), + 'Wrong event emitted', + ); + }).timeout(TEST_TIMEOUT * 2); + + test('Detect when an environment has been deleted', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const { executable, envDir } = await venvs.create('one'); + await setupLocator(async (e) => { + if (e.type === FileChangeType.Deleted) { + actualEvent = e; + deferred.resolve(); + } + }); + + // VSCode API has a limitation where it fails to fire event when environment folder is deleted directly: + // https://github.com/microsoft/vscode/issues/110923 + // Using chokidar directly in tests work, but it has permission issues on Windows that you cannot delete a + // folder if it has a subfolder that is being watched inside: https://github.com/paulmillr/chokidar/issues/422 + // Hence we test directly deleting the executable, and not the whole folder using `workspaceVenvs.cleanUp()`. + await venvs.delete(executable); + await waitForChangeToBeDetected(deferred); + if (!options?.doNotVerifyIfLocated) { + const isFound = await isLocated(executable); + assert.notOk(isFound); + } + + assert.notStrictEqual(actualEvent!, undefined, 'Wrong event emitted'); + if (options?.kind) { + assert.strictEqual(actualEvent!.kind, options.kind, 'Wrong event emitted'); + } + assert.notStrictEqual(actualEvent!.searchLocation, undefined, 'Wrong event emitted'); + assert.ok( + externalDeps.arePathsSame(actualEvent!.searchLocation!.fsPath, path.dirname(envDir)), + 'Wrong event emitted', + ); + }).timeout(TEST_TIMEOUT * 2); + + test('Detect when an environment has been updated', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + // Create a dummy environment so we can update its executable later. We can't choose a real environment here. + // Executables inside real environments can be symlinks, so writing on them can result in the real executable + // being updated instead of the symlink. + const { executable, envDir } = await venvs.createDummyEnv('one', options?.kind); + await setupLocator(async (e) => { + if (e.type === FileChangeType.Changed) { + actualEvent = e; + deferred.resolve(); + } + }); + + await venvs.update(executable); + await waitForChangeToBeDetected(deferred); + assert.notStrictEqual(actualEvent!, undefined, 'Event was not emitted'); + if (options?.kind) { + assert.strictEqual(actualEvent!.kind, options.kind, 'Kind is not as expected'); + } + assert.notStrictEqual(actualEvent!.searchLocation, undefined, 'Search location is not set'); + assert.ok( + externalDeps.arePathsSame(actualEvent!.searchLocation!.fsPath, path.dirname(envDir)), + `Paths don't match ${actualEvent!.searchLocation!.fsPath} != ${path.dirname(envDir)}`, + ); + }).timeout(TEST_TIMEOUT * 2); +} diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts new file mode 100644 index 000000000000..07a7a864ef74 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts @@ -0,0 +1,275 @@ +// 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 { expect } from 'chai'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import * as winreg from '../../../../../client/pythonEnvironments/common/windowsRegistry'; +import { + WindowsRegistryLocator, + WINDOWS_REG_PROVIDER_ID, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; + +suite('Windows Registry', () => { + let stubReadRegistryValues: sinon.SinonStub; + let stubReadRegistryKeys: sinon.SinonStub; + let locator: WindowsRegistryLocator; + + const regTestRoot = path.join(TEST_LAYOUT_ROOT, 'winreg'); + + const registryData = { + x64: { + HKLM: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore', '\\SOFTWARE\\Python\\ContinuumAnalytics'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9', + values: { + '': '', + DisplayName: 'Python 3.9 (64-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '64bit', + SysVersion: '3.9', + Version: '3.9.0rc2', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'py39', 'python.exe'), + }, + subKeys: [] as string[], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics', + values: { + '': '', + }, + subKeys: ['\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64'], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64', + values: { + '': '', + DisplayName: 'Anaconda py38_4.8.3', + SupportUrl: 'github.com/continuumio/anaconda-issues', + SysArchitecture: '64bit', + SysVersion: '3.8', + Version: 'py38_4.8.3', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'conda3', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + HKCU: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.7'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.7', + values: { + '': '', + DisplayName: 'Python 3.7 (64-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '64bit', + SysVersion: '3.7', + Version: '3.7.7', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.7\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.7\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'python37', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + }, + x86: { + HKLM: [], + HKCU: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8', + values: { + '': '', + DisplayName: 'Python 3.8 (32-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '32bit', + SysVersion: '3.8.5', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'python38', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + }, + }; + + function fakeRegistryValues({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryValue[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const values: winreg.IRegistryValue[] = []; + for (const [name, value] of Object.entries(k.values)) { + values.push({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: k.key, + name, + type: winreg.REG_SZ, + value: value ?? '', + }); + } + return Promise.resolve(values); + } + } + return Promise.resolve([]); + } + + function fakeRegistryKeys({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryKey[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const keys = k.subKeys.map((s) => ({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: s, + })); + return Promise.resolve(keys); + } + } + return Promise.resolve([]); + } + + setup(async () => { + sinon.stub(externalDependencies, 'inExperiment').returns(true); + stubReadRegistryValues = sinon.stub(winreg, 'readRegistryValues'); + stubReadRegistryKeys = sinon.stub(winreg, 'readRegistryKeys'); + stubReadRegistryValues.callsFake(fakeRegistryValues); + stubReadRegistryKeys.callsFake(fakeRegistryKeys); + + locator = new WindowsRegistryLocator(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('iterEnvs()', async () => { + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'py39', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'conda3', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python37', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python38', 'python.exe')), + ].map((e) => ({ ...e, source: [PythonEnvSource.WindowsRegistry] })); + + const lazyIterator = locator.iterEnvs(undefined, true); + const envs = await getEnvs(lazyIterator); + expect(envs.length).to.equal(0); + + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): query is undefined', async () => { + // Iterate no envs when query is `undefined`, i.e notify completion immediately. + const lazyIterator = locator.iterEnvs(undefined, true); + const envs = await getEnvs(lazyIterator); + expect(envs.length).to.equal(0); + }); + + test('iterEnvs(): no registry permission', async () => { + stubReadRegistryKeys.callsFake(() => { + throw Error(); + }); + + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); + const actualEnvs = await getEnvs(iterator); + + assert.deepStrictEqual(actualEnvs, []); + }); + + test('iterEnvs(): partial registry permission', async () => { + stubReadRegistryKeys.callsFake(({ arch, hive, key }: winreg.Options) => { + if (hive === winreg.HKLM) { + throw Error(); + } + return fakeRegistryKeys({ arch, hive, key }); + }); + + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python37', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python38', 'python.exe')), + ].map((e) => ({ ...e, source: [PythonEnvSource.WindowsRegistry] })); + + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..60168f4847ca --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.testvirtualenvs.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { WorkspaceVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { testLocatorWatcher } from './watcherTestUtils'; + +suite('WorkspaceVirtualEnvironment Locator', async () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + testLocatorWatcher(testWorkspaceFolder, async (root?: string) => new WorkspaceVirtualEnvironmentLocator(root!), { + arg: testWorkspaceFolder, + kind: PythonEnvKind.Venv, + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.unit.test.ts new file mode 100644 index 000000000000..3bf93b1eaf5d --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { WorkspaceVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { createBasicEnv } from '../../common'; + +suite('WorkspaceVirtualEnvironment Locator', () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + let getOSTypeStub: sinon.SinonStub; + let watchLocationForPatternStub: sinon.SinonStub; + let locator: WorkspaceVirtualEnvironmentLocator; + + setup(() => { + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOSTypeStub.returns(platformUtils.OSType.Linux); + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + locator = new WorkspaceVirtualEnvironmentLocator(testWorkspaceFolder); + }); + teardown(async () => { + await locator.dispose(); + sinon.restore(); + }); + + test('iterEnvs(): Windows', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testWorkspaceFolder, 'win1', 'python.exe')), + createBasicEnv( + PythonEnvKind.Venv, + path.join(testWorkspaceFolder, '.direnv', 'win2', 'Scripts', 'python.exe'), + ), + createBasicEnv(PythonEnvKind.Pipenv, path.join(testWorkspaceFolder, '.venv', 'Scripts', 'python.exe')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows', async () => { + getOSTypeStub.returns(platformUtils.OSType.Linux); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testWorkspaceFolder, '.direnv', 'posix1virtualenv', 'bin', 'python'), + ), + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix2conda', 'python')), + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix3custom', 'bin', 'python')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); 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..bcb3cfbed7f7 --- /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.strictEqual(event, expected); + }); + + KINDS_TO_TEST.forEach((kind) => { + test(`non-empty event ("${kind}")`, () => { + const expected: PythonEnvsChangedEvent = { + kind, + searchLocation: location, + }; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(event, expected); + }); + + KINDS_TO_TEST.forEach((kind) => { + test(`non-empty event ("${kind}")`, () => { + const expected: BasicPythonEnvsChangedEvent = { + kind, + }; + const watcher = new PythonEnvsWatcher<BasicPythonEnvsChangedEvent>(); + let event: BasicPythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(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..6ccc6451fa55 --- /dev/null +++ b/src/test/pythonEnvironments/base/watchers.unit.test.ts @@ -0,0 +1,55 @@ +// 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 { 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); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/commonTestConstants.ts b/src/test/pythonEnvironments/common/commonTestConstants.ts new file mode 100644 index 000000000000..361f9b4dd1ea --- /dev/null +++ b/src/test/pythonEnvironments/common/commonTestConstants.ts @@ -0,0 +1,27 @@ +import * as path from 'path'; + +export const TEST_LAYOUT_ROOT = path.join( + __dirname, + '..', + '..', + '..', + '..', + 'src', + 'test', + 'pythonEnvironments', + 'common', + 'envlayouts', +); + +export const TEST_DATA_ROOT = path.join( + __dirname, + '..', + '..', + '..', + '..', + 'src', + 'test', + 'pythonEnvironments', + 'common', + 'testdata', +); diff --git a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts new file mode 100644 index 000000000000..647a17a40a90 --- /dev/null +++ b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts @@ -0,0 +1,543 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { getOSType, OSType } from '../../../client/common/utils/platform'; +import { findInterpretersInDir } from '../../../client/pythonEnvironments/common/commonUtils'; +import { ensureFSTree as utilEnsureFSTree } from '../../utils/fs'; + +const IS_WINDOWS = getOSType() === OSType.Windows; + +async function ensureFSTree(tree: string): Promise<void> { + await utilEnsureFSTree(tree.trimEnd(), __dirname); +} + +suite('pyenvs common utils - finding Python executables', () => { + const datadir = path.join(__dirname, '.data'); + + function resolveDataFiles(rootName: string, relnames: string[]): string[] { + return relnames.map((relname) => path.normalize(`${datadir}/${rootName}/${relname}`)); + } + + async function find( + rootName: string, + maxDepth?: number, + filterDir?: (x: string) => boolean, + // Errors are helpful when testing, so we don't bother ignoring them. + ): Promise<string[]> { + const results: string[] = []; + const root = path.join(datadir, rootName); + const executables = findInterpretersInDir(root, maxDepth, filterDir); + for await (const entry of executables) { + results.push(entry.filename); + } + return results; + } + + suite('mixed', () => { + const rootName = 'root_mixed'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + sub1/ + spam + sub2/ + sub2.1/ + sub2.1.1/ + <python.exe> + spam.txt + sub2.2/ + <spam.exe> + python3.exe + sub3/ + python.exe + <spam.exe> + spam.txt + <python.exe> + eggs.exe + python2.exe + <python3.8.exe> + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + sub1/ + spam + sub2/ + sub2.1/ + sub2.1.1/ + <python> + spam.txt + sub2.2/ + <spam> + python3 + sub3/ + python + <spam> + spam.txt + <python> + eggs + python2 + <python3.8> + python3 -> sub2/sub2.2/python3 + python3.7 -> sub2/sub2.1/sub2.1.1/python + `); + } + }); + + suite('non-recursive', () => { + test('no filter', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These will match. + 'python.exe', + 'python2.exe', + 'python3.8.exe', + ] + : [ + // These will match. + 'python', + 'python2', + 'python3', + 'python3.7', + 'python3.8', + ], + ); + + const found = await find(rootName); + + assert.deepEqual(found, expected); + }); + }); + + suite('recursive', () => { + test('no filter', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These will match. + 'python.exe', + 'python2.exe', + 'python3.8.exe', + 'sub2/sub2.1/sub2.1.1/python.exe', + 'sub2/sub2.2/python3.exe', + 'sub3/python.exe', + ] + : [ + // These will match. + 'python', + 'python2', + 'python3', + 'python3.7', + 'python3.8', + 'sub2/sub2.1/sub2.1.1/python', + 'sub2/sub2.2/python3', + 'sub3/python', + ], + ); + + const found = await find(rootName, 3); + + assert.deepEqual(found, expected); + }); + + test('filtered', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These will match. + 'python.exe', + 'python2.exe', + 'python3.8.exe', + 'sub3/python.exe', + ] + : [ + // These will match. + 'python', + 'python2', + 'python3', + 'python3.7', + 'python3.8', + 'sub3/python', + ], + ); + function filterDir(dirname: string): boolean { + return dirname.match(/sub\d$/) !== null; + } + + const found = await find(rootName, 3, filterDir); + + assert.deepEqual(found, expected); + }); + }); + }); + + suite('different layouts and patterns', () => { + suite('names', () => { + const rootName = 'root_name_patterns'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + <python.exe> + <python2.exe> + <python2.7.exe> + <python27.exe> + <python3.exe> + <python3.8.exe> + <python3.8.1.exe> # should match but doesn't + <python3.8.1rc1.exe> # should match but doesn't + <python3.8.1rc1.10213.exe> # should match but doesn't + <python3.8.1-candidate1.exe> # should match but doesn't + <python.3.8.exe> # should match but doesn't + <python.3.8.1.candidate.1.exe> # should match but doesn't + <python-3.exe> # should match but doesn't + <python-3.8.exe> # should match but doesn't + <python38.exe> + <python381.exe> + <my-python.exe> # should match but doesn't + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + <python> + <python2> + <python2.7> + <python27> + <python3> + <python3.8> + <python3.8.1> # should match but doesn't + <python3.8.1rc1> # should match but doesn't + <python3.8.1rc1.10213> # should match but doesn't + <python3.8.1-candidate1> # should match but doesn't + <python.3.8> # should match but doesn't + <python.3.8.1.candidate.1> # should match but doesn't + <python-3> # should match but doesn't + <python-3.8> # should match but doesn't + <python38> + <python381> + <my-python> # should match but doesn't + `); + } + }); + + test('non-recursive', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These order here matters. + 'python.exe', + 'python2.7.exe', + 'python2.exe', + 'python27.exe', + 'python3.8.exe', + 'python3.exe', + 'python38.exe', + 'python381.exe', + ] + : [ + // These order here matters. + 'python', + 'python2', + 'python2.7', + 'python27', + 'python3', + 'python3.8', + 'python38', + 'python381', + ], + ); + + const found = await find(rootName); + + assert.deepEqual(found, expected); + }); + }); + + suite('trees', () => { + const rootName = 'root_layouts'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + py/ + 2.7/ + bin/ + <python.exe> + <python2.exe> + <python2.7.exe> + 3.8/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + python/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + 3.8/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + python2/ + <python.exe> + python3/ + <python3.exe> + python3.8/ + bin/ + <python3.exe> + <python3.8.exe> + python38/ + bin/ + <python3.exe> + python.3.8/ + bin/ + <python3.exe> + <python3.8.exe> + python-3.8/ + bin/ + <python3.exe> + <python3.8.exe> + my-python/ + <python3.exe> + 3.8/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + py/ + 2.7/ + bin/ + <python> + <python2> + <python2.7> + 3.8/ + bin/ + <python> + <python3> + <python3.8> + python/ + bin/ + <python> + <python3> + <python3.8> + 3.8/ + bin/ + <python> + <python3> + <python3.8> + python2/ + <python> + python3/ + <python3> + python3.8/ + bin/ + <python3> + <python3.8> + python38/ + bin/ + <python3> + python.3.8/ + bin/ + <python3> + <python3.8> + python-3.8/ + bin/ + <python3> + <python3.8> + my-python/ + <python3> + 3.8/ + bin/ + <python> + <python3> + <python3.8> + `); + } + }); + + test('recursive', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These order here matters. + '3.8/bin/python.exe', + '3.8/bin/python3.8.exe', + '3.8/bin/python3.exe', + 'my-python/python3.exe', + 'py/2.7/bin/python.exe', + 'py/2.7/bin/python2.7.exe', + 'py/2.7/bin/python2.exe', + 'py/3.8/bin/python.exe', + 'py/3.8/bin/python3.8.exe', + 'py/3.8/bin/python3.exe', + 'python/3.8/bin/python.exe', + 'python/3.8/bin/python3.8.exe', + 'python/3.8/bin/python3.exe', + 'python/bin/python.exe', + 'python/bin/python3.8.exe', + 'python/bin/python3.exe', + 'python-3.8/bin/python3.8.exe', + 'python-3.8/bin/python3.exe', + 'python.3.8/bin/python3.8.exe', + 'python.3.8/bin/python3.exe', + 'python2/python.exe', + 'python3/python3.exe', + 'python3.8/bin/python3.8.exe', + 'python3.8/bin/python3.exe', + 'python38/bin/python3.exe', + ] + : [ + // These order here matters. + '3.8/bin/python', + '3.8/bin/python3', + '3.8/bin/python3.8', + 'my-python/python3', + 'py/2.7/bin/python', + 'py/2.7/bin/python2', + 'py/2.7/bin/python2.7', + 'py/3.8/bin/python', + 'py/3.8/bin/python3', + 'py/3.8/bin/python3.8', + 'python/3.8/bin/python', + 'python/3.8/bin/python3', + 'python/3.8/bin/python3.8', + 'python/bin/python', + 'python/bin/python3', + 'python/bin/python3.8', + 'python-3.8/bin/python3', + 'python-3.8/bin/python3.8', + 'python.3.8/bin/python3', + 'python.3.8/bin/python3.8', + 'python2/python', + 'python3/python3', + 'python3.8/bin/python3', + 'python3.8/bin/python3.8', + 'python38/bin/python3', + ], + ); + + const found = await find(rootName, 3); + + assert.deepEqual(found, expected); + }); + }); + }); + + suite('tricky cases', () => { + const rootName = 'root_tricky'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + pythons/ + <python.exe> + <python2.exe> + <python2.7.exe> + <python3.exe> + <python3.7.exe> + <python3.8.exe> + <python3.9.2.exe> # should match but doesn't + <python3.10a1.exe> # should match but doesn't + python2.7.exe/ + <spam.exe> + python3.8.exe/ + <python.exe> + <py.exe> # launcher not supported + <py3.exe> # launcher not supported + <Python3.exe> # case-insensitive + <PYTHON.EXE> # case-insensitive + <Python3> + <PYTHON> + <not-python.exe> + <python.txt> + <python> + <python2> + <python3> + <spam.exe> + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + pythons/ + <python> + <python2> + <python2.7> + <python3> + <python3.7> + <python3.8> + <python3.9.2> # should match but doesn't + <python3.10a1> # should match but doesn't + <py> # launcher not supported + <py3> # launcher not supported + <Python3> + <PYTHON> + <not-python> + <python.txt> + <python.exe> + <python2.exe> + <python3.exe> + <spam> + `); + } + }); + + test('recursive', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These order here matters. + 'python3.8.exe/python.exe', + 'pythons/python.exe', + 'pythons/python2.7.exe', + 'pythons/python2.exe', + 'pythons/python3.7.exe', + 'pythons/python3.8.exe', + 'pythons/python3.exe', + // 'Python3.exe', + // 'PYTHON.EXE', + ] + : [ + // These order here matters. + 'pythons/python', + 'pythons/python2', + 'pythons/python2.7', + 'pythons/python3', + 'pythons/python3.7', + 'pythons/python3.8', + ], + ); + + const found = await find(rootName, 3); + + assert.deepEqual(found, expected); + }); + }); +}); 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..af719c3e40ed --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -0,0 +1,374 @@ +// 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 { PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { identifyEnvironment } from '../../../client/pythonEnvironments/common/environmentIdentifier'; +import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; +import { getOSType as getOSTypeForTest, OSType } from '../../common'; +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: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.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: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.Conda); + }); + }); + + suite('Pipenv', () => { + let getEnvVar: sinon.SinonStub; + let readFile: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + }); + + test('Path to a global pipenv environment', async () => { + const expectedDotProjectFile = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project2-vnNIWe9P', + '.project', + ); + const expectedProjectFile = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2'); + readFile.withArgs(expectedDotProjectFile).resolves(expectedProjectFile); + const interpreterPath: string = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project2-vnNIWe9P', + 'bin', + 'python', + ); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + + assert.strictEqual(envType, PythonEnvKind.Pipenv); + }); + + test('Path to a local pipenv environment with a custom Pipfile name', async () => { + getEnvVar.withArgs('PIPENV_PIPFILE').returns('CustomPipfileName'); + const interpreterPath: string = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'project1', + '.venv', + 'Scripts', + 'python.exe', + ); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + + assert.strictEqual(envType, PythonEnvKind.Pipenv); + }); + }); + + suite('Microsoft Store', () => { + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + const fakeLocalAppDataPath = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + 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); + + pathExists = sinon.stub(externalDependencies, 'pathExists'); + pathExists.withArgs(path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', 'idle.exe')).resolves(true); + }); + suiteTeardown(() => { + getEnvVar.restore(); + pathExists.restore(); + }); + executable.forEach((exe) => { + test(`Path to local app data microsoft store interpreter (${exe})`, async () => { + getEnvVar.withArgs('LOCALAPPDATA').returns(fakeLocalAppDataPath); + const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path to local app data microsoft store interpreter app sub-directory (${exe})`, async () => { + getEnvVar.withArgs('LOCALAPPDATA').returns(fakeLocalAppDataPath); + const interpreterPath = path.join( + fakeLocalAppDataPath, + 'Microsoft', + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path to program files microsoft store interpreter app sub-directory (${exe})`, async () => { + const interpreterPath = path.join( + fakeProgramFilesPath, + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Local app data not set (${exe})`, async () => { + getEnvVar.withArgs('LOCALAPPDATA').returns(undefined); + const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Program files app data not set (${exe})`, async () => { + const interpreterPath = path.join( + fakeProgramFilesPath, + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + getEnvVar.withArgs('ProgramFiles').returns(undefined); + pathExists.withArgs(path.join(path.dirname(interpreterPath), 'idle.exe')).resolves(true); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path using forward slashes (${exe})`, async () => { + const interpreterPath = path + .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) + .replace(/\\/g, '/'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path using long path style slashes (${exe})`, async () => { + const interpreterPath = path + .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) + .replace('\\', '/'); + pathExists.callsFake((p: string) => { + if (p.endsWith('idle.exe')) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + const envType: PythonEnvKind = await identifyEnvironment(`\\\\?\\${interpreterPath}`); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + }); + }); + + suite('Pyenv', () => { + let getEnvVarStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + + suiteSetup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformApis, 'getOSType'); + getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir'); + }); + + suiteTeardown(() => { + getEnvVarStub.restore(); + getOsTypeStub.restore(); + getUserHomeDirStub.restore(); + }); + + test('PYENV_ROOT is not set on non-Windows, fallback to the default value ~/.pyenv', async function () { + if (getOSTypeForTest() === OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'pyenv1', + '.pyenv', + 'versions', + '3.6.9', + 'bin', + 'python', + ); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv1')); + getEnvVarStub.withArgs('PYENV_ROOT').returns(undefined); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + + test('PYENV is not set on Windows, fallback to the default value %USERPROFILE%\\.pyenv\\pyenv-win', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'pyenv2', + '.pyenv', + 'pyenv-win', + 'versions', + '3.6.9', + 'bin', + 'python.exe', + ); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv2')); + getEnvVarStub.withArgs('PYENV').returns(undefined); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + + test('PYENV_ROOT is set to a custom value on non-Windows', async function () { + if (getOSTypeForTest() === OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python'); + + getEnvVarStub.withArgs('PYENV_ROOT').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3')); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + + test('PYENV is set to a custom value on Windows', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python.exe'); + + getEnvVarStub.withArgs('PYENV').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3')); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + }); + + suite('Venv', () => { + test('Pyvenv.cfg is in the same directory as the interpreter', async () => { + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv1', 'python'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.Venv); + }); + test('Pyvenv.cfg is in the same directory as the interpreter', async () => { + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv2', 'bin', 'python'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.Venv); + }); + }); + + suite('Virtualenvwrapper', () => { + let getEnvVarStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + + suiteSetup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformApis, 'getOSType'); + getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir'); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper1')); + }); + + suiteTeardown(() => { + getEnvVarStub.restore(); + getOsTypeStub.restore(); + getUserHomeDirStub.restore(); + }); + + test('WORKON_HOME is set to its default value ~/.virtualenvs on non-Windows', async function () { + if (getOSTypeForTest() === OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'virtualenvwrapper1', + '.virtualenvs', + 'myenv', + 'bin', + 'python', + ); + + getEnvVarStub.withArgs('WORKON_HOME').returns(undefined); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnvWrapper); + + return undefined; + }); + + test('WORKON_HOME is set to its default value %USERPROFILE%\\Envs on Windows', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'virtualenvwrapper1', + 'Envs', + 'myenv', + 'Scripts', + 'python', + ); + + getEnvVarStub.withArgs('WORKON_HOME').returns(undefined); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnvWrapper); + + return undefined; + }); + + test('WORKON_HOME is set to a custom value', async () => { + const workonHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper2'); + const interpreterPath = path.join(workonHomeDir, 'myenv', 'bin', 'python'); + + getEnvVarStub.withArgs('WORKON_HOME').returns(workonHomeDir); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnvWrapper); + }); + }); + + suite('Virtualenv', () => { + const activateFiles = [ + { folder: 'virtualenv1', file: 'activate' }, + { folder: 'virtualenv2', file: 'activate.sh' }, + { folder: 'virtualenv3', file: 'activate.ps1' }, + ]; + + activateFiles.forEach(({ folder, file }) => { + test(`Folder contains ${file}`, async () => { + const interpreterPath = path.join(TEST_LAYOUT_ROOT, folder, 'bin', 'python'); + const envType = await identifyEnvironment(interpreterPath); + + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnv); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts new file mode 100644 index 000000000000..23eebc5fee07 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import { getOSType, OSType } from '../../../../client/common/utils/platform'; +import { isActiveStateEnvironment } from '../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('isActiveStateEnvironment Tests', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + + test('Return true if runtime is set up', async () => { + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(true); + }); + + test(`Return false if the runtime is not set up`, async () => { + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'b6a0705d', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(false); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts new file mode 100644 index 000000000000..9480dffe6a59 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -0,0 +1,675 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { assert, expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as util from 'util'; +import { eq } from 'semver'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import * as platform from '../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../client/pythonEnvironments/base/locatorUtils'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import * as windowsUtils from '../../../../client/pythonEnvironments/common/windowsUtils'; +import { Conda, CondaInfo } from '../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { CondaEnvironmentLocator } from '../../../../client/pythonEnvironments/base/locators/lowLevel/condaLocator'; +import { createBasicEnv } from '../../base/common'; +import { assertBasicEnvsEqual } from '../../base/locators/envTestUtils'; +import { OUTPUT_MARKER_SCRIPT } from '../../../../client/common/process/internal/scripts'; + +suite('Conda and its environments are located correctly', () => { + // getOSType() is stubbed to return this. + let osType: platform.OSType; + + // getUserHomeDir() is stubbed to return this. + let homeDir: string | undefined; + + // getRegistryInterpreters() is stubbed to return this. + let registryInterpreters: windowsUtils.IRegistryInterpreterData[]; + + // readdir() and readFile() are stubbed to present a dummy file system based on this + // object graph. Keys are filenames. For each key, if the corresponding value is an + // object, it's considered a subdirectory, otherwise it's a file with that value as + // its contents. + type Directory = { [fileName: string]: string | Directory | undefined }; + let files: Directory; + + function getFile(filePath: string): string | Directory | undefined; + function getFile(filePath: string, throwIfMissing: 'throwIfMissing'): string | Directory; + function getFile(filePath: string, throwIfMissing?: 'throwIfMissing') { + const segments = filePath.split(/[\\/]/); + let dir: Directory | string = files; + let currentPath = ''; + for (const fileName of segments) { + if (typeof dir === 'string') { + throw new Error(`${currentPath} is not a directory`); + } else if (fileName !== '') { + const child: string | Directory | undefined = dir[fileName]; + if (child === undefined) { + if (throwIfMissing) { + const err: NodeJS.ErrnoException = new Error(`${currentPath} does not contain ${fileName}`); + err.code = 'ENOENT'; + throw err; + } else { + return undefined; + } + } + dir = child; + currentPath = `${currentPath}/${fileName}`; + } + } + return dir; + } + + // exec("command") is stubbed such that if either getFile(`${entry}/command`) or + // getFile(`${entry}/command.exe`) returns a non-empty string, it succeeds with + // that string as stdout. Otherwise, the exec stub throws. Empty strings can be + // used to simulate files that are present but not executable. + let execPath: string[]; + + async function expectConda(expectedPath: string) { + const expectedInfo = JSON.parse(getFile(expectedPath) as string); + + const conda = await Conda.getConda(); + expect(conda).to.not.equal(undefined, 'conda should not be missing'); + + const info = await conda!.getInfo(); + expect(info).to.deep.equal(expectedInfo); + } + + function condaInfo(condaVersion?: string): CondaInfo { + return { + conda_version: condaVersion, + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + default_prefix: '/conda/base', + envs: [], + }; + } + + let getPythonSetting: sinon.SinonStub; + let condaVersionOutput: string; + + setup(() => { + osType = platform.OSType.Unknown; + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.withArgs('condaPath').returns('conda'); + homeDir = undefined; + execPath = []; + files = {}; + registryInterpreters = []; + + sinon.stub(windowsUtils, 'getRegistryInterpreters').callsFake(async () => registryInterpreters); + + sinon.stub(platform, 'getOSType').callsFake(() => osType); + + sinon.stub(platform, 'getUserHomeDir').callsFake(() => homeDir); + + sinon.stub(fs, 'lstat').callsFake(async (filePath: fs.PathLike) => { + if (typeof filePath !== 'string') { + throw new Error(`expected filePath to be string, got ${typeof filePath}`); + } + const file = getFile(filePath, 'throwIfMissing'); + return { + isDirectory: () => typeof file !== 'string', + } as fs.Stats; + }); + + sinon.stub(fs, 'pathExists').callsFake(async (filePath: string | Buffer) => { + if (typeof filePath !== 'string') { + throw new Error(`expected filePath to be string, got ${typeof filePath}`); + } + try { + getFile(filePath, 'throwIfMissing'); + } catch { + return false; + } + return true; + }); + + sinon.stub(fs, 'readdir').callsFake( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (filePath: fs.PathLike, options?: { withFileTypes?: boolean }): Promise<any> => { + if (typeof filePath !== 'string') { + throw new Error(`expected path to be string, got ${typeof path}`); + } + + const dir = getFile(filePath, 'throwIfMissing'); + if (typeof dir === 'string') { + throw new Error(`${path} is not a directory`); + } + + if (options === undefined) { + return (Object.keys(getFile(filePath, 'throwIfMissing')) as unknown) as fs.Dirent[]; + } + + const names = Object.keys(dir); + if (!options?.withFileTypes) { + return names; + } + + return names.map( + (name): fs.Dirent => { + const isFile = typeof dir[name] === 'string'; + return { + name, + path: dir.name?.toString() ?? '', + isFile: () => isFile, + isDirectory: () => !isFile, + isBlockDevice: () => false, + isCharacterDevice: () => false, + isSymbolicLink: () => false, + isFIFO: () => false, + isSocket: () => false, + parentPath: '', + }; + }, + ); + }, + ); + const readFileStub = async ( + filePath: fs.PathOrFileDescriptor, + options: { encoding: BufferEncoding; flag?: string | undefined } | BufferEncoding, + ): Promise<string> => { + if (typeof filePath !== 'string') { + throw new Error(`expected filePath to be string, got ${typeof filePath}`); + } else if (typeof options === 'string') { + if (options !== 'utf8') { + throw new Error(`Unsupported encoding ${options}`); + } + } else if ((options as any).encoding !== 'utf8') { + throw new Error(`Unsupported encoding ${(options as any).encoding}`); + } + + const contents = getFile(filePath); + if (typeof contents !== 'string') { + throw new Error(`${filePath} is not a file`); + } + + return contents; + }; + sinon.stub(fs, 'readFile' as any).callsFake(readFileStub as any); + + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + for (const prefix of ['', ...execPath]) { + const contents = getFile(path.join(prefix, command)); + if (args[0] === 'info' && args[1] === '--json') { + if (typeof contents === 'string' && contents !== '') { + return { stdout: contents }; + } + } else if (args[0] === '--version') { + return { stdout: condaVersionOutput }; + } else { + throw new Error(`Invalid arguments: ${util.inspect(args)}`); + } + } + throw new Error(`${command} is missing or is not executable`); + }); + }); + + teardown(() => { + condaVersionOutput = ''; + sinon.restore(); + }); + + suite('Conda binary is located correctly', () => { + test('Must not find conda if it is missing', async () => { + const conda = await Conda.getConda(); + expect(conda).to.equal(undefined, 'conda should be missing'); + }); + + test('Must find conda using `python.condaPath` setting and prefer it', async () => { + getPythonSetting.withArgs('condaPath').returns('condaPath/conda'); + + files = { + condaPath: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + }; + await expectConda('/condaPath/conda'); + }); + + test('Must find conda on PATH, and prefer it', async () => { + osType = platform.OSType.Linux; + execPath = ['/bin']; + + files = { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + opt: { + anaconda: { + bin: { + conda: JSON.stringify(condaInfo('4.8.1')), + }, + }, + }, + }; + + await expectConda('/bin/conda'); + }); + + test('Use conda.bat when possible over conda.exe on windows', async () => { + osType = platform.OSType.Windows; + + getPythonSetting.withArgs('condaPath').returns('bin/conda'); + files = { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + condabin: { + 'conda.bat': JSON.stringify(condaInfo('4.8.0')), + }, + }; + + await expectConda('/condabin/conda.bat'); + }); + + suite('Must find conda in well-known locations', () => { + const condaDirNames = ['Anaconda', 'anaconda', 'Miniconda', 'miniconda']; + + condaDirNames.forEach((condaDirName) => { + suite(`Must find conda in well-known locations on Linux with ${condaDirName} directory name`, () => { + setup(() => { + osType = platform.OSType.Linux; + homeDir = '/home/user'; + + files = { + home: { + user: { + opt: {}, + }, + }, + opt: { + homebrew: { + bin: {}, + }, + }, + usr: { + share: { + doc: {}, + }, + local: { + share: { + doc: {}, + }, + }, + }, + }; + }); + + [ + '/usr/share', + '/usr/local/share', + '/opt', + '/opt/homebrew/bin', + '/home/user', + '/home/user/opt', + ].forEach((prefix) => { + const condaPath = `${prefix}/${condaDirName}`; + + test(`Must find conda in ${condaPath}`, async () => { + const prefixDir = getFile(prefix) as Directory; + prefixDir[condaDirName] = { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + }; + + await expectConda(`${condaPath}/bin/conda`); + }); + }); + }); + + suite(`Must find conda in well-known locations on Windows with ${condaDirName} directory name`, () => { + setup(() => { + osType = platform.OSType.Windows; + homeDir = 'E:\\Users\\user'; + + sinon + .stub(platform, 'getEnvironmentVariable') + .withArgs('PROGRAMDATA') + .returns('D:\\ProgramData') + .withArgs('LOCALAPPDATA') + .returns('F:\\Users\\user\\AppData\\Local'); + + files = { + 'C:': {}, + 'D:': { + ProgramData: {}, + }, + 'E:': { + Users: { + user: {}, + }, + }, + 'F:': { + Users: { + user: { + AppData: { + Local: { + Continuum: {}, + }, + }, + }, + }, + }, + }; + }); + + // Drive letters are intentionally unusual to ascertain that locator doesn't hardcode paths. + ['D:\\ProgramData', 'E:\\Users\\user', 'F:\\Users\\user\\AppData\\Local\\Continuum'].forEach( + (prefix) => { + const condaPath = `${prefix}\\${condaDirName}`; + + test(`Must find conda in ${condaPath}`, async () => { + const prefixDir = getFile(prefix) as Directory; + prefixDir[condaDirName] = { + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.0')), + }, + }; + + await expectConda(`${condaPath}\\Scripts\\conda.exe`); + }); + }, + ); + }); + }); + }); + + suite('Must find conda in environments.txt', () => { + test('Must find conda in environments.txt on Unix', async () => { + osType = platform.OSType.Linux; + homeDir = '/home/user'; + + files = { + home: { + user: { + '.conda': { + 'environments.txt': ['', '/missing', '', '# comment', '', ' /present ', ''].join( + '\n', + ), + }, + }, + }, + present: { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + }, + }; + + await expectConda('/present/bin/conda'); + }); + + test('Must find conda in environments.txt on Windows', async () => { + osType = platform.OSType.Windows; + homeDir = 'D:\\Users\\user'; + + files = { + 'D:': { + Users: { + user: { + '.conda': { + 'environments.txt': [ + '', + 'C:\\Missing', + '', + '# comment', + '', + ' E:\\Present ', + '', + ].join('\r\n'), + }, + }, + }, + }, + 'E:': { + Present: { + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.0')), + }, + }, + }, + }; + + await expectConda('E:\\Present\\Scripts\\conda.exe'); + }); + }); + + test('Must find conda in the registry', async () => { + osType = platform.OSType.Windows; + + registryInterpreters = [ + { + interpreterPath: 'C:\\Python2\\python.exe', + }, + { + interpreterPath: 'C:\\Anaconda2\\python.exe', + distroOrgName: 'ContinuumAnalytics', + }, + { + interpreterPath: 'C:\\Python3\\python.exe', + distroOrgName: 'PythonCore', + }, + { + interpreterPath: 'C:\\Anaconda3\\python.exe', + distroOrgName: 'ContinuumAnalytics', + }, + ]; + + files = { + 'C:': { + Python3: { + // Shouldn't be located because it's not a well-known conda path, + // and it's listed under PythonCore in the registry. + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.0')), + }, + }, + Anaconda2: { + // Shouldn't be located because it can't handle "conda info --json". + Scripts: { + 'conda.exe': '', + }, + }, + Anaconda3: { + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.1')), + }, + }, + }, + }; + + await expectConda('C:\\Anaconda3\\Scripts\\conda.exe'); + }); + }); + + test('Conda version returns version info using `conda info` command if applicable', async () => { + files = { + conda: JSON.stringify(condaInfo('4.8.0')), + }; + const conda = await Conda.getConda(); + const version = await conda?.getCondaVersion(); + expect(version).to.not.equal(undefined); + expect(eq(version!, '4.8.0')).to.equal(true); + }); + + test('Conda version returns version info using `conda --version` command otherwise', async () => { + files = { + conda: JSON.stringify(condaInfo()), + }; + condaVersionOutput = 'conda 4.8.0'; + const conda = await Conda.getConda(); + const version = await conda?.getCondaVersion(); + expect(version).to.not.equal(undefined); + expect(eq(version!, '4.8.0')).to.equal(true); + }); + + test('Conda version works for dev versions of conda', async () => { + files = { + conda: JSON.stringify(condaInfo('23.1.0.post7+d5281f611')), + }; + condaVersionOutput = 'conda 23.1.0.post7+d5281f611'; + const conda = await Conda.getConda(); + const version = await conda?.getCondaVersion(); + expect(version).to.not.equal(undefined); + expect(eq(version!, '23.1.0')).to.equal(true); + }); + + test('Conda run args returns `undefined` for conda version below 4.9.0', async () => { + files = { + conda: JSON.stringify(condaInfo('4.8.0')), + }; + const conda = await Conda.getConda(); + const args = await conda?.getRunPythonArgs({ name: 'envName', prefix: 'envPrefix' }); + expect(args).to.equal(undefined); + }); + + test('Conda run args returns appropriate args for conda version starting with 4.9.0', async () => { + files = { + conda: JSON.stringify(condaInfo('4.9.0')), + }; + const conda = await Conda.getConda(); + let args = await conda?.getRunPythonArgs({ name: 'envName', prefix: 'envPrefix' }); + expect(args).to.not.equal(undefined); + assert.deepStrictEqual( + args, + ['conda', 'run', '-p', 'envPrefix', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + 'Incorrect args for case 1', + ); + + args = await conda?.getRunPythonArgs({ name: '', prefix: 'envPrefix' }); + assert.deepStrictEqual( + args, + ['conda', 'run', '-p', 'envPrefix', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + 'Incorrect args for case 2', + ); + }); + + suite('Conda env list is parsed correctly', () => { + setup(() => { + homeDir = '/home/user'; + files = { + home: { + user: { + miniconda3: { + bin: { + python: '', + conda: JSON.stringify({ + conda_version: '4.8.0', + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + root_prefix: '/home/user/miniconda3', + default_prefix: '/home/user/miniconda3/envs/env1', + envs_dirs: ['/home/user/miniconda3/envs', '/home/user/.conda/envs'], + envs: [ + '/home/user/miniconda3', + '/home/user/miniconda3/envs/env1', + '/home/user/miniconda3/envs/env2', + '/home/user/miniconda3/envs/dir/env3', + '/home/user/.conda/envs/env4', + '/home/user/.conda/envs/env5', + '/env6', + ], + }), + }, + envs: { + env1: { + bin: { + python: '', + }, + }, + dir: { + env3: { + bin: { + python: '', + }, + }, + }, + }, + }, + '.conda': { + envs: { + env4: { + bin: { + python: '', + }, + }, + }, + }, + }, + }, + env6: { + bin: { + python: '', + }, + }, + }; + sinon.stub(externalDependencies, 'inExperiment').returns(false); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Must compute conda environment name from prefix', async () => { + const conda = new Conda('/home/user/miniconda3/bin/conda'); + const envs = await conda.getEnvList(); + + expect(envs).to.have.deep.members([ + { + prefix: '/home/user/miniconda3', + name: 'base', + }, + { + prefix: '/home/user/miniconda3/envs/env1', + name: 'env1', + }, + { + prefix: '/home/user/miniconda3/envs/env2', + name: 'env2', + }, + { + prefix: '/home/user/miniconda3/envs/dir/env3', + name: undefined, // because it's not directly under envsDirs + }, + { + prefix: '/home/user/.conda/envs/env4', + name: 'env4', + }, + { + prefix: '/home/user/.conda/envs/env5', + name: 'env5', + }, + { + prefix: '/env6', + name: undefined, // because it's not directly under envsDirs + }, + ]); + }); + + test('Must iterate conda environments correctly', async () => { + const locator = new CondaEnvironmentLocator(); + const envs = await getEnvs(locator.iterEnvs()); + const expected = [ + '/home/user/miniconda3', + '/home/user/miniconda3/envs/env1', + '/home/user/miniconda3/envs/dir/env3', + '/home/user/.conda/envs/env4', + '/env6', + ].map((envPath) => + createBasicEnv(PythonEnvKind.Conda, path.join(envPath, 'bin', 'python'), undefined, envPath), + ); + expected.push( + ...[ + '/home/user/miniconda3/envs/env2', // Show env2 despite there's no bin/python* under it + '/home/user/.conda/envs/env5', // Show env5 despite there's no bin/python* under it + ].map((envPath) => createBasicEnv(PythonEnvKind.Conda, 'python', undefined, envPath)), + ); + assertBasicEnvsEqual(envs, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts new file mode 100644 index 000000000000..5d348aa2b131 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { Hatch } from '../../../../client/pythonEnvironments/common/environmentManagers/hatch'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +export type HatchCommand = { cmd: 'env show --json' } | { cmd: 'env find'; env: string } | { cmd: null }; + +export function hatchCommand(args: string[]): HatchCommand { + if (args.length < 2) { + return { cmd: null }; + } + if (args[0] === 'env' && args[1] === 'show' && args[2] === '--json') { + return { cmd: 'env show --json' }; + } + if (args[0] === 'env' && args[1] === 'find') { + return { cmd: 'env find', env: args[2] }; + } + return { cmd: null }; +} + +interface VerifyOptions { + path?: boolean; + cwd?: string; +} + +export function makeExecHandler(venvDirs: Record<string, string>, verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise<ExecutionResult<string>> => { + if (verify.path && file !== 'hatch') { + throw new Error('Command failed'); + } + if (verify.cwd) { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (!cwd || !externalDependencies.arePathsSame(cwd, verify.cwd)) { + throw new Error('Command failed'); + } + } + const cmd = hatchCommand(args); + if (cmd.cmd === 'env show --json') { + const envs = Object.fromEntries(Object.keys(venvDirs).map((name) => [name, { type: 'virtual' }])); + return { stdout: JSON.stringify(envs) }; + } + if (cmd.cmd === 'env find' && cmd.env in venvDirs) { + return { stdout: venvDirs[cmd.env] }; + } + throw new Error('Command failed'); + }; +} + +const testHatchDir = path.join(TEST_LAYOUT_ROOT, 'hatch'); +// This is usually in <data-dir>/hatch, e.g. `~/.local/share/hatch` +const hatchEnvsDir = path.join(testHatchDir, 'env/virtual/python'); +export const projectDirs = { + project1: path.join(testHatchDir, 'project1'), + project2: path.join(testHatchDir, 'project2'), +}; +export const venvDirs = { + project1: { default: path.join(hatchEnvsDir, 'cK2g6fIm/project1') }, + project2: { + default: path.join(hatchEnvsDir, 'q4In3tK-/project2'), + test: path.join(hatchEnvsDir, 'q4In3tK-/test'), + }, +}; + +suite('Hatch binary is located correctly', async () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + exec = sinon.stub(externalDependencies, 'exec'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testPath = async (verify = true) => { + // If `verify` is false, don’t verify that the command has been called with that path + exec.callsFake( + makeExecHandler(venvDirs.project1, verify ? { path: true, cwd: projectDirs.project1 } : undefined), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal('hatch'); + }; + + test('Use Hatch on PATH if available', () => testPath()); + + test('Return undefined if Hatch cannot be found', async () => { + getPythonSetting.returns('hatch'); + exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.unit.test.ts new file mode 100644 index 000000000000..59bbf5e53167 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.unit.test.ts @@ -0,0 +1,41 @@ +// 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 { getMicrosoftStorePythonExes } from '../../../../client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator'; +import { isMicrosoftStoreDir } from '../../../../client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('Microsoft Store Env', () => { + let getEnvVarStub: sinon.SinonStub; + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVarStub.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + getEnvVarStub.restore(); + }); + + test('Store Python Interpreters', async () => { + const expected = [path.join(testStoreAppRoot, 'python3.7.exe'), path.join(testStoreAppRoot, 'python3.8.exe')]; + + const actual = await getMicrosoftStorePythonExes(); + assert.deepEqual(actual, expected); + }); + + test('isMicrosoftStoreDir: valid case', () => { + assert.deepStrictEqual(isMicrosoftStoreDir(testStoreAppRoot), true); + assert.deepStrictEqual(isMicrosoftStoreDir(testStoreAppRoot + path.sep), true); + }); + + test('isMicrosoftStoreDir: invalid case', () => { + assert.deepStrictEqual(isMicrosoftStoreDir(__dirname), false); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pipenv.functional.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pipenv.functional.test.ts new file mode 100644 index 000000000000..33d2a9eb1fe4 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pipenv.functional.test.ts @@ -0,0 +1,47 @@ +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 externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { isPipenvEnvironmentRelatedToFolder } from '../../../../client/pythonEnvironments/common/environmentManagers/pipenv'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('Pipenv utils', () => { + let readFile: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + }); + + test('Global pipenv environment is associated with a project whose Pipfile lies at 3 levels above the project', async () => { + getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5'); + const expectedDotProjectFile = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project3-2s1eXEJ2', + '.project', + ); + const project = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project3'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + const interpreterPath: string = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project3-2s1eXEJ2', + 'Scripts', + 'python.exe', + ); + const folder = path.join(project, 'parent', 'child', 'folder'); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, true); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pipenv.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pipenv.unit.test.ts new file mode 100644 index 000000000000..8a30ca5153ae --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pipenv.unit.test.ts @@ -0,0 +1,278 @@ +import * as assert from 'assert'; +import * as pathModule from 'path'; +import * as sinon from 'sinon'; +import * as platformApis from '../../../../client/common/utils/platform'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + _getAssociatedPipfile, + isPipenvEnvironment, + isPipenvEnvironmentRelatedToFolder, +} from '../../../../client/pythonEnvironments/common/environmentManagers/pipenv'; + +const path = platformApis.getOSType() === platformApis.OSType.Windows ? pathModule.win32 : pathModule.posix; + +suite('Pipenv helper', () => { + suite('isPipenvEnvironmentRelatedToFolder()', async () => { + let readFile: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + let arePathsSame: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + arePathsSame = sinon.stub(externalDependencies, 'arePathsSame'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + pathExists.restore(); + arePathsSame.restore(); + }); + + test('If no Pipfile is associated with the environment, return false', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + // Dot project file doesn't exist + pathExists.withArgs(expectedDotProjectFile).resolves(false); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, false); + }); + + test('If a Pipfile is associated with the environment but no pipfile is associated with the folder, return false', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + const project = path.join('path', 'to', 'project'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + pathExists.withArgs(project).resolves(true); + const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + // Pipfile associated with folder doesn't exist + pathExists.withArgs(pipFileAssociatedWithFolder).resolves(false); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, false); + }); + + test('If a Pipfile is associated with the environment and another is associated with the folder, but the path to both Pipfiles are different, return false', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + const project = path.join('path', 'to', 'project'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + pathExists.withArgs(project).resolves(true); + const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + // Pipfile associated with folder exists + pathExists.withArgs(pipFileAssociatedWithFolder).resolves(true); + // But the paths to both Pipfiles aren't the same + arePathsSame.withArgs(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder).resolves(false); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, false); + }); + + test('If a Pipfile is associated with the environment and another is associated with the folder, and the path to both Pipfiles are same, return true', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + const project = path.join('path', 'to', 'project'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + pathExists.withArgs(project).resolves(true); + const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + // Pipfile associated with folder exists + pathExists.withArgs(pipFileAssociatedWithFolder).resolves(true); + // The paths to both Pipfiles are also the same + arePathsSame.withArgs(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder).resolves(true); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, true); + }); + }); + + suite('isPipenvEnvironment()', async () => { + let readFile: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + pathExists.restore(); + }); + + test('If the project layout matches that of a local pipenv environment, return true', async () => { + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + // Environment is inside the project + const interpreterPath = path.join(project, '.venv', 'Scripts', 'python.exe'); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, true); + }); + + test('If not local & dotProject file is missing, return false', async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + // dotProject file doesn't exist + pathExists.withArgs(expectedDotProjectFile).resolves(false); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test('If not local & dotProject contains invalid path to project, return false', async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const project = path.join('path', 'to', 'project'); + // Project doesn't exist + pathExists.withArgs(project).resolves(false); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + // dotProject file doesn't exist + pathExists.withArgs(expectedDotProjectFile).resolves(false); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + readFile.withArgs(expectedDotProjectFile).resolves(project); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test("If not local & the name of the project isn't used as a prefix in the environment folder, return false", async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + // The project name (someProjectName) isn't used as a prefix in environment folder name (project-2s1eXEJ2) + const project = path.join('path', 'to', 'someProjectName'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + readFile.withArgs(expectedDotProjectFile).resolves(project); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test('If the project layout matches that of a global pipenv environment, return true', async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + readFile.withArgs(expectedDotProjectFile).resolves(project); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, true); + }); + }); + + suite('_getAssociatedPipfile()', async () => { + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + }); + + teardown(() => { + getEnvVar.restore(); + pathExists.restore(); + }); + + test('Correct Pipfile is returned for folder whose Pipfile lies in the folder directory', async () => { + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + pathExists.withArgs(pipFile).resolves(true); + const folder = project; + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: false }); + + assert.strictEqual(result, pipFile); + }); + + test('Correct Pipfile is returned for folder if a custom Pipfile name is being used', async () => { + getEnvVar.withArgs('PIPENV_PIPFILE').returns('CustomPipfile'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'CustomPipfile'); + pathExists.withArgs(pipFile).resolves(true); + const folder = project; + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: false }); + + assert.strictEqual(result, pipFile); + }); + + test('Correct Pipfile is returned for folder whose Pipfile lies 3 levels above the folder', async () => { + getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + pathExists.withArgs(pipFile).resolves(true); + const folder = path.join(project, 'parent', 'child', 'folder'); + pathExists.withArgs(folder).resolves(true); + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: true }); + + assert.strictEqual(result, pipFile); + }); + + test('No Pipfile is returned for folder if no Pipfile exists in the associated directories', async () => { + getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile doesn't exist + pathExists.withArgs(pipFile).resolves(false); + const folder = path.join(project, 'parent', 'child', 'folder'); + pathExists.withArgs(folder).resolves(true); + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: true }); + + assert.strictEqual(result, undefined); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts new file mode 100644 index 000000000000..0cbc6b25145c --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts @@ -0,0 +1,147 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; +import { getPixi } from '../../../../client/pythonEnvironments/common/environmentManagers/pixi'; + +export type PixiCommand = { cmd: 'info --json' } | { cmd: '--version' } | { cmd: null }; + +const textPixiDir = path.join(TEST_LAYOUT_ROOT, 'pixi'); +export const projectDirs = { + windows: { + path: path.join(textPixiDir, 'windows'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'windows', '.pixi', 'envs', 'default'), + }, + ], + }, + }, + nonWindows: { + path: path.join(textPixiDir, 'non-windows'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'non-windows', '.pixi', 'envs', 'default'), + }, + ], + }, + }, + multiEnv: { + path: path.join(textPixiDir, 'multi-env'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'default'), + }, + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'py310'), + }, + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'py311'), + }, + ], + }, + }, +}; + +/** + * Convert the command line arguments into a typed command. + */ +export function pixiCommand(args: string[]): PixiCommand { + if (args[0] === '--version') { + return { cmd: '--version' }; + } + + if (args.length < 2) { + return { cmd: null }; + } + if (args[0] === 'info' && args[1] === '--json') { + return { cmd: 'info --json' }; + } + return { cmd: null }; +} +interface VerifyOptions { + pixiPath?: string; + cwd?: string; +} + +export function makeExecHandler(verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise<ExecutionResult<string>> => { + /// Verify that the executable path is indeed the one we expect it to be + if (verify.pixiPath && file !== verify.pixiPath) { + throw new Error('Command failed: not the correct pixi path'); + } + + const cmd = pixiCommand(args); + if (cmd.cmd === '--version') { + return { stdout: 'pixi 0.24.1' }; + } + + /// Verify that the working directory is the expected one + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (verify.cwd) { + if (!cwd || !externalDependencies.arePathsSame(cwd, verify.cwd)) { + throw new Error(`Command failed: not the correct path, expected: ${verify.cwd}, got: ${cwd}`); + } + } + + /// Convert the command into a single string + if (cmd.cmd === 'info --json') { + const project = Object.values(projectDirs).find((p) => cwd?.startsWith(p.path)); + if (!project) { + throw new Error('Command failed: could not find project'); + } + return { stdout: JSON.stringify(project.info) }; + } + + throw new Error(`Command failed: unknown command ${args}`); + }; +} + +suite('Pixi binary is located correctly', async () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let pathExists: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + exec = sinon.stub(externalDependencies, 'exec'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testPath = async (pixiPath: string, verify = true) => { + getPythonSetting.returns(pixiPath); + pathExists.returns(pixiPath !== 'pixi'); + // If `verify` is false, don’t verify that the command has been called with that path + exec.callsFake(makeExecHandler(verify ? { pixiPath } : undefined)); + const pixi = await getPixi(); + + if (pixiPath === 'pixi') { + expect(pixi).to.equal(undefined); + } else { + expect(pixi?.command).to.equal(pixiPath); + } + }; + + test('Return a Pixi instance in an empty directory', () => testPath('pixiPath', false)); + test('When user has specified a valid Pixi path, use it', () => testPath('path/to/pixi/binary')); + // 'pixi' is the default value + test('When user hasn’t specified a path, use Pixi on PATH if available', () => testPath('pixi')); + + test('Return undefined if Pixi cannot be found', async () => { + getPythonSetting.returns('pixi'); + exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + const pixi = await getPixi(); + expect(pixi?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts new file mode 100644 index 000000000000..5e40e3454e2b --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as platformApis from '../../../../client/common/utils/platform'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { isPoetryEnvironment, Poetry } from '../../../../client/pythonEnvironments/common/environmentManagers/poetry'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +const testPoetryDir = path.join(TEST_LAYOUT_ROOT, 'poetry'); +const project1 = path.join(testPoetryDir, 'project1'); +const project4 = path.join(testPoetryDir, 'project4'); +const project3 = path.join(testPoetryDir, 'project3'); + +suite('isPoetryEnvironment Tests', () => { + let shellExecute: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + suite('Global poetry environment', async () => { + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + }); + teardown(() => { + sinon.restore(); + }); + test('Return true if environment folder name matches global env pattern and environment is of virtual env type', async () => { + const result = await isPoetryEnvironment( + path.join(testPoetryDir, 'poetry-tutorial-project-6hnqYwvD-py3.8', 'Scripts', 'python.exe'), + ); + expect(result).to.equal(true); + }); + + test('Return false if environment folder name does not matches env pattern', async () => { + const result = await isPoetryEnvironment( + path.join(testPoetryDir, 'wannabeglobalenv', 'Scripts', 'python.exe'), + ); + expect(result).to.equal(false); + }); + + test('Return false if environment folder name matches env pattern but is not of virtual env type', async () => { + const result = await isPoetryEnvironment( + path.join(testPoetryDir, 'project1-haha-py3.8', 'Scripts', 'python.exe'), + ); + expect(result).to.equal(false); + }); + }); + + suite('Local poetry environment', async () => { + setup(() => { + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('poetry'); + shellExecute.callsFake((command: string, _options: ShellOptions) => { + if (command === 'poetry env list --full-path') { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Return true if environment folder name matches criteria for local envs', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + const result = await isPoetryEnvironment(path.join(project1, '.venv', 'Scripts', 'python.exe')); + expect(result).to.equal(true); + }); + + test(`Return false if environment folder name is not named '.venv' for local envs`, async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + const result = await isPoetryEnvironment(path.join(project1, '.venv2', 'Scripts', 'python.exe')); + expect(result).to.equal(false); + }); + + test(`Return false if running poetry for project dir as cwd fails (pyproject.toml file is invalid)`, async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + const result = await isPoetryEnvironment(path.join(project4, '.venv', 'bin', 'python')); + expect(result).to.equal(false); + }); + }); +}); + +suite('Poetry binary is located correctly', async () => { + let shellExecute: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + }); + + teardown(() => { + sinon.restore(); + }); + + test("Return undefined if pyproject.toml doesn't exist in cwd", async () => { + getPythonSetting.returns('poetryPath'); + shellExecute.callsFake((_command: string, _options: ShellOptions) => + Promise.resolve<ExecutionResult<string>>({ stdout: '' }), + ); + + const poetry = await Poetry.getPoetry(testPoetryDir); + + expect(poetry?.command).to.equal(undefined); + }); + + test('Return undefined if cwd contains pyproject.toml which does not contain a poetry section', async () => { + getPythonSetting.returns('poetryPath'); + shellExecute.callsFake((_command: string, _options: ShellOptions) => + Promise.resolve<ExecutionResult<string>>({ stdout: '' }), + ); + + const poetry = await Poetry.getPoetry(project3); + + expect(poetry?.command).to.equal(undefined); + }); + + test('When user has specified a valid poetry path, use it', async () => { + getPythonSetting.returns('poetryPath'); + shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if ( + command === `poetryPath env list --full-path` && + cwd && + externalDependencies.arePathsSame(cwd, project1) + ) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal('poetryPath'); + }); + + test("When user hasn't specified a path, use poetry on PATH if available", async () => { + getPythonSetting.returns('poetry'); // Setting returns the default value + shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (command === `poetry env list --full-path` && cwd && externalDependencies.arePathsSame(cwd, project1)) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal('poetry'); + }); + + test('When poetry is not available on PATH, try using the default poetry location if valid', async () => { + const home = platformApis.getUserHomeDir(); + if (!home) { + assert(true); + return; + } + const defaultPoetry = path.join(home, '.poetry', 'bin', 'poetry'); + const pathExistsSync = sinon.stub(externalDependencies, 'pathExistsSync'); + pathExistsSync.withArgs(defaultPoetry).returns(true); + pathExistsSync.callThrough(); + getPythonSetting.returns('poetry'); + shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if ( + command === `${defaultPoetry} env list --full-path` && + cwd && + externalDependencies.arePathsSame(cwd, project1) + ) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal(defaultPoetry); + }); + + test('Return undefined otherwise', async () => { + getPythonSetting.returns('poetry'); + shellExecute.callsFake((_command: string, _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pyenv.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pyenv.unit.test.ts new file mode 100644 index 000000000000..e5902ae2b291 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pyenv.unit.test.ts @@ -0,0 +1,305 @@ +// 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 platformUtils from '../../../../client/common/utils/platform'; +import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + IPyenvVersionStrings, + isPyenvEnvironment, + isPyenvShimDir, + parsePyenvVersion, +} from '../../../../client/pythonEnvironments/common/environmentManagers/pyenv'; + +suite('Pyenv Identifier Tests', () => { + const home = platformUtils.getUserHomeDir() || ''; + let getEnvVariableStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformUtils, 'getOSType'); + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + }); + + teardown(() => { + getEnvVariableStub.restore(); + pathExistsStub.restore(); + getOsTypeStub.restore(); + }); + + type PyenvUnitTestData = { + testTitle: string; + interpreterPath: string; + pyenvEnvVar?: string; + osType: platformUtils.OSType; + }; + + const testData: PyenvUnitTestData[] = [ + { + testTitle: 'undefined', + interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'undefined', + interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + osType: platformUtils.OSType.Windows, + }, + { + testTitle: 'its default value', + interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home, '.pyenv'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'its default value', + interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home, '.pyenv', 'pyenv-win'), + osType: platformUtils.OSType.Windows, + }, + { + testTitle: 'a custom value', + interpreterPath: path.join('path', 'to', 'mypyenv', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join('path', 'to', 'mypyenv'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'a custom value', + interpreterPath: path.join('path', 'to', 'mypyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join('path', 'to', 'mypyenv', 'pyenv-win'), + osType: platformUtils.OSType.Windows, + }, + ]; + + testData.forEach(({ testTitle, interpreterPath, pyenvEnvVar, osType }) => { + test(`The environment variable is set to ${testTitle} on ${osType}, and the interpreter path is in a subfolder of the pyenv folder`, async () => { + getEnvVariableStub.withArgs('PYENV_ROOT').returns(pyenvEnvVar); + getEnvVariableStub.withArgs('PYENV').returns(pyenvEnvVar); + getOsTypeStub.returns(osType); + pathExistsStub.resolves(true); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, true); + }); + }); + + test('The pyenv directory does not exist', async () => { + const interpreterPath = path.join('path', 'to', 'python'); + + pathExistsStub.resolves(false); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test('The interpreter path is not in a subfolder of the pyenv folder', async () => { + const interpreterPath = path.join('path', 'to', 'python'); + + pathExistsStub.resolves(true); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); +}); + +suite('Pyenv Versions Parser Test', () => { + interface IPyenvVersionTestData { + input: string; + expectedOutput?: IPyenvVersionStrings; + } + const testData: IPyenvVersionTestData[] = [ + { input: '2.7.0', expectedOutput: { pythonVer: '2.7.0', distro: undefined, distroVer: undefined } }, + { input: '2.7-dev', expectedOutput: { pythonVer: '2.7-dev', distro: undefined, distroVer: undefined } }, + { input: '2.7.18', expectedOutput: { pythonVer: '2.7.18', distro: undefined, distroVer: undefined } }, + { input: '3.9.0', expectedOutput: { pythonVer: '3.9.0', distro: undefined, distroVer: undefined } }, + { input: '3.9-dev', expectedOutput: { pythonVer: '3.9-dev', distro: undefined, distroVer: undefined } }, + { input: '3.10-dev', expectedOutput: { pythonVer: '3.10-dev', distro: undefined, distroVer: undefined } }, + { + input: 'activepython-2.7.14', + expectedOutput: { pythonVer: undefined, distro: 'activepython', distroVer: '2.7.14' }, + }, + { + input: 'activepython-3.6.0', + expectedOutput: { pythonVer: undefined, distro: 'activepython', distroVer: '3.6.0' }, + }, + { input: 'anaconda-4.0.0', expectedOutput: { pythonVer: undefined, distro: 'anaconda', distroVer: '4.0.0' } }, + { input: 'anaconda2-5.3.1', expectedOutput: { pythonVer: undefined, distro: 'anaconda2', distroVer: '5.3.1' } }, + { + input: 'anaconda2-2019.07', + expectedOutput: { pythonVer: undefined, distro: 'anaconda2', distroVer: '2019.07' }, + }, + { input: 'anaconda3-5.3.1', expectedOutput: { pythonVer: undefined, distro: 'anaconda3', distroVer: '5.3.1' } }, + { + input: 'anaconda3-2020.07', + expectedOutput: { pythonVer: undefined, distro: 'anaconda3', distroVer: '2020.07' }, + }, + { + input: 'graalpython-20.2.0', + expectedOutput: { pythonVer: undefined, distro: 'graalpython', distroVer: '20.2.0' }, + }, + { input: 'ironpython-dev', expectedOutput: { pythonVer: undefined, distro: 'ironpython', distroVer: 'dev' } }, + { + input: 'ironpython-2.7.6.3', + expectedOutput: { pythonVer: undefined, distro: 'ironpython', distroVer: '2.7.6.3' }, + }, + { + input: 'ironpython-2.7.7', + expectedOutput: { pythonVer: undefined, distro: 'ironpython', distroVer: '2.7.7' }, + }, + { input: 'jython-dev', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: 'dev' } }, + { input: 'jython-2.5.0', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.5.0' } }, + { input: 'jython-2.5-dev', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.5-dev' } }, + { + input: 'jython-2.5.4-rc1', + expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.5.4-rc1' }, + }, + { input: 'jython-2.7.2', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.7.2' } }, + { input: 'micropython-dev', expectedOutput: { pythonVer: undefined, distro: 'micropython', distroVer: 'dev' } }, + { + input: 'micropython-1.9.3', + expectedOutput: { pythonVer: undefined, distro: 'micropython', distroVer: '1.9.3' }, + }, + { + input: 'micropython-1.13', + expectedOutput: { pythonVer: undefined, distro: 'micropython', distroVer: '1.13' }, + }, + { + input: 'miniconda-latest', + expectedOutput: { pythonVer: undefined, distro: 'miniconda', distroVer: 'latest' }, + }, + { input: 'miniconda-2.2.2', expectedOutput: { pythonVer: undefined, distro: 'miniconda', distroVer: '2.2.2' } }, + { + input: 'miniconda-3.18.3', + expectedOutput: { pythonVer: undefined, distro: 'miniconda', distroVer: '3.18.3' }, + }, + { + input: 'miniconda2-latest', + expectedOutput: { pythonVer: undefined, distro: 'miniconda2', distroVer: 'latest' }, + }, + { + input: 'miniconda2-4.7.12', + expectedOutput: { pythonVer: undefined, distro: 'miniconda2', distroVer: '4.7.12' }, + }, + { + input: 'miniconda3-latest', + expectedOutput: { pythonVer: undefined, distro: 'miniconda3', distroVer: 'latest' }, + }, + { + input: 'miniconda3-4.7.12', + expectedOutput: { pythonVer: undefined, distro: 'miniconda3', distroVer: '4.7.12' }, + }, + { + input: 'miniforge3-4.9.2', + expectedOutput: { pythonVer: undefined, distro: 'miniforge3', distroVer: '4.9.2' }, + }, + { + input: 'pypy-c-jit-latest', + expectedOutput: { pythonVer: undefined, distro: 'pypy-c-jit', distroVer: 'latest' }, + }, + { + input: 'pypy-c-nojit-latest', + expectedOutput: { pythonVer: undefined, distro: 'pypy-c-nojit', distroVer: 'latest' }, + }, + { input: 'pypy-dev', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: 'dev' } }, + { input: 'pypy-stm-2.3', expectedOutput: { pythonVer: undefined, distro: 'pypy-stm', distroVer: '2.3' } }, + { input: 'pypy-stm-2.5.1', expectedOutput: { pythonVer: undefined, distro: 'pypy-stm', distroVer: '2.5.1' } }, + { input: 'pypy-5.4-src', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.4-src' } }, + { input: 'pypy-5.4', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.4' } }, + { input: 'pypy-5.7.1-src', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.7.1-src' } }, + { input: 'pypy-5.7.1', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.7.1' } }, + { input: 'pypy2-5.4-src', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4-src' } }, + { input: 'pypy2-5.4', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4' } }, + { input: 'pypy2-5.4.1-src', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4.1-src' } }, + { input: 'pypy2-5.4.1', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4.1' } }, + { input: 'pypy2.7-7.3.1-src', expectedOutput: { pythonVer: '2.7', distro: 'pypy', distroVer: '7.3.1-src' } }, + { input: 'pypy2.7-7.3.1', expectedOutput: { pythonVer: '2.7', distro: 'pypy', distroVer: '7.3.1' } }, + { input: 'pypy3-2.4.0-src', expectedOutput: { pythonVer: '3', distro: 'pypy', distroVer: '2.4.0-src' } }, + { input: 'pypy3-2.4.0', expectedOutput: { pythonVer: '3', distro: 'pypy', distroVer: '2.4.0' } }, + { + input: 'pypy3.3-5.2-alpha1-src', + expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.2-alpha1-src' }, + }, + { input: 'pypy3.3-5.2-alpha1', expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.2-alpha1' } }, + { + input: 'pypy3.3-5.5-alpha-src', + expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.5-alpha-src' }, + }, + { input: 'pypy3.3-5.5-alpha', expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.5-alpha' } }, + { + input: 'pypy3.5-c-jit-latest', + expectedOutput: { pythonVer: '3.5', distro: 'pypy-c-jit', distroVer: 'latest' }, + }, + { + input: 'pypy3.5-5.7-beta-src', + expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7-beta-src' }, + }, + { input: 'pypy3.5-5.7-beta', expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7-beta' } }, + { + input: 'pypy3.5-5.7.1-beta-src', + expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7.1-beta-src' }, + }, + { input: 'pypy3.5-5.7.1-beta', expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7.1-beta' } }, + { input: 'pypy3.6-7.3.1-src', expectedOutput: { pythonVer: '3.6', distro: 'pypy', distroVer: '7.3.1-src' } }, + { input: 'pypy3.6-7.3.1', expectedOutput: { pythonVer: '3.6', distro: 'pypy', distroVer: '7.3.1' } }, + { + input: 'pypy3.7-v7.3.5rc3-win64', + expectedOutput: { pythonVer: '3.7', distro: 'pypy', distroVer: '7.3.5rc3-win64' }, + }, + { + input: 'pypy3.7-v7.3.5-win64', + expectedOutput: { pythonVer: '3.7', distro: 'pypy', distroVer: '7.3.5-win64' }, + }, + { + input: 'pypy-5.7.1-beta-src', + expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.7.1-beta-src' }, + }, + { input: 'pypy', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: undefined } }, + { input: 'pyston-0.6.1', expectedOutput: { pythonVer: undefined, distro: 'pyston', distroVer: '0.6.1' } }, + { input: 'stackless-dev', expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: 'dev' } }, + { + input: 'stackless-2.7-dev', + expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: '2.7-dev' }, + }, + { + input: 'stackless-3.4-dev', + expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: '3.4-dev' }, + }, + { input: 'stackless-3.7.5', expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: '3.7.5' } }, + { input: 'stackless', expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: undefined } }, + { input: 'unknown', expectedOutput: undefined }, + ]; + + testData.forEach((data) => { + test(`Parse pyenv version [${data.input}]`, async () => { + assert.deepStrictEqual(parsePyenvVersion(data.input), data.expectedOutput); + }); + }); +}); + +suite('Pyenv Shims Dir filter tests', () => { + let getEnvVariableStub: sinon.SinonStub; + const pyenvRoot = path.join('path', 'to', 'pyenv', 'root'); + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getEnvVariableStub.withArgs('PYENV_ROOT').returns(pyenvRoot); + }); + + teardown(() => { + getEnvVariableStub.restore(); + }); + + test('isPyenvShimDir: valid case', () => { + assert.deepStrictEqual(isPyenvShimDir(path.join(pyenvRoot, 'shims')), true); + }); + test('isPyenvShimDir: invalid case', () => { + assert.deepStrictEqual(isPyenvShimDir(__dirname), false); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts new file mode 100644 index 000000000000..6d75668b8556 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts @@ -0,0 +1,227 @@ +// 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 fsapi from '../../../../client/common/platform/fs-paths'; +import * as platformUtils from '../../../../client/common/utils/platform'; +import { PythonReleaseLevel, PythonVersion } from '../../../../client/pythonEnvironments/base/info'; +import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + getPythonVersionFromPyvenvCfg, + isVenvEnvironment, + isVirtualenvEnvironment, + isVirtualenvwrapperEnvironment, +} from '../../../../client/pythonEnvironments/common/environmentManagers/simplevirtualenvs'; +import { TEST_DATA_ROOT, TEST_LAYOUT_ROOT } from '../commonTestConstants'; +import { assertVersionsEqual } from '../../base/locators/envTestUtils'; + +suite('isVenvEnvironment Tests', () => { + const pyvenvCfg = 'pyvenv.cfg'; + const envRoot = path.join('path', 'to', 'env'); + const configPath = path.join('env', pyvenvCfg); + let fileExistsStub: sinon.SinonStub; + + setup(() => { + fileExistsStub = sinon.stub(fileUtils, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('pyvenv.cfg does not exist', async () => { + const interpreter = path.join(envRoot, 'python'); + fileExistsStub.callsFake(() => Promise.resolve(false)); + assert.ok(!(await isVenvEnvironment(interpreter))); + }); + + test('pyvenv.cfg exists in the current folder', async () => { + const interpreter = path.join(envRoot, 'python'); + + fileExistsStub.callsFake((p: string) => { + if (p.endsWith(configPath)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + + assert.ok(await isVenvEnvironment(interpreter)); + }); + + test('pyvenv.cfg exists in the parent folder', async () => { + const interpreter = path.join(envRoot, 'bin', 'python'); + + fileExistsStub.callsFake((p: string) => { + if (p.endsWith(configPath)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + + assert.ok(await isVenvEnvironment(interpreter)); + }); +}); + +suite('isVirtualenvEnvironment Tests', () => { + const envRoot = path.join('path', 'to', 'env'); + const interpreter = path.join(envRoot, 'python'); + let readDirStub: sinon.SinonStub; + + setup(() => { + readDirStub = sinon.stub(fsapi, 'readdir'); + }); + + teardown(() => { + readDirStub.restore(); + }); + + test('Interpreter folder contains an activate file', async () => { + readDirStub.resolves(['activate', 'python']); + + assert.ok(await isVirtualenvEnvironment(interpreter)); + }); + + test('Interpreter folder does not contain any activate.* files', async () => { + readDirStub.resolves(['mymodule', 'python']); + + assert.strictEqual(await isVirtualenvEnvironment(interpreter), false); + }); +}); + +suite('isVirtualenvwrapperEnvironment Tests', () => { + const homeDir = path.join(TEST_LAYOUT_ROOT, 'virutalhome'); + + let getEnvVariableStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readDirStub: sinon.SinonStub; + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + + readDirStub = sinon.stub(fsapi, 'readdir'); + readDirStub.resolves(['activate', 'python']); + + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + pathExistsStub.resolves(true); + // This is windows specific path. For test purposes we will use the common path + // that works on all OS. So, fail the path check for windows specific default route. + pathExistsStub.withArgs(path.join(homeDir, 'Envs')).resolves(false); + }); + + teardown(() => { + getEnvVariableStub.restore(); + getUserHomeDirStub.restore(); + pathExistsStub.restore(); + readDirStub.restore(); + }); + + test('WORKON_HOME is not set, and the interpreter is in a sub-folder of virtualenvwrapper', async () => { + const interpreter = path.join(homeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + getUserHomeDirStub.returns(homeDir); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('WORKON_HOME is set to a custom value, and the interpreter is is in a sub-folder', async () => { + const workonHomeDirectory = path.join(homeDir, 'workonhome'); + const interpreter = path.join(workonHomeDirectory, 'win2', 'bin', 'python.exe'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + pathExistsStub.withArgs(path.join(workonHomeDirectory)).resolves(true); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('The interpreter is not in a sub-folder of WORKON_HOME', async () => { + const workonHomeDirectory = path.join('path', 'to', 'workonhome'); + const interpreter = path.join('some', 'path', 'env', 'bin', 'python'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + + assert.deepStrictEqual(await isVirtualenvwrapperEnvironment(interpreter), false); + }); +}); + +suite('Virtual Env Version Parser Tests', () => { + let readFileStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + const testDataRoot = path.join(TEST_DATA_ROOT, 'versiondata', 'venv'); + + setup(() => { + readFileStub = sinon.stub(fileUtils, 'readFile'); + + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + pathExistsStub.resolves(true); + }); + + teardown(() => { + readFileStub.restore(); + pathExistsStub.restore(); + }); + + interface ICondaPythonVersionTestData { + name: string; + historyFileContents: string; + expected: PythonVersion | undefined; + } + + function getTestData(): ICondaPythonVersionTestData[] { + const data: ICondaPythonVersionTestData[] = []; + + const cases = fsapi.readdirSync(testDataRoot).map((c) => path.join(testDataRoot, c)); + const casesToVersion = new Map<string, PythonVersion>(); + casesToVersion.set('case1', { major: 3, minor: 9, micro: 0 }); + + casesToVersion.set('case2', { + major: 3, + minor: 8, + micro: 2, + release: { level: PythonReleaseLevel.Final, serial: 0 }, + sysVersion: undefined, + }); + casesToVersion.set('case3', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Candidate, serial: 1 }, + }); + casesToVersion.set('case4', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Alpha, serial: 1 }, + sysVersion: undefined, + }); + + for (const c of cases) { + const name = path.basename(c); + const expected = casesToVersion.get(name); + if (expected) { + data.push({ + name, + historyFileContents: fsapi.readFileSync(c, 'utf-8'), + expected, + }); + } + } + + return data; + } + + const testData = getTestData(); + testData.forEach((data) => { + test(`Parsing ${data.name}`, async () => { + readFileStub.resolves(data.historyFileContents); + + const actual = await getPythonVersionFromPyvenvCfg('/path/here/does/not/matter'); + + assertVersionsEqual(actual, data.expected); + }); + }); +}); diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe 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/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/bin/dummy similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/bin/dummy diff --git a/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/conda-meta/history new file mode 100644 index 000000000000..a329d0a79b88 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/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/envsWithoutPython/python.exe b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonFiles/environments/conda/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg new file mode 100644 index 000000000000..365d6f5eacee --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.11 +include-system-site-packages = false +version = 3.11.1 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml new file mode 100644 index 000000000000..9848374b54fd --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml @@ -0,0 +1,6 @@ +# this file is not actually used in tests, as all is mocked out + +# The default environment always exists +#[envs.default] + +[envs.test] diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/.project b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/.project new file mode 100644 index 000000000000..f16530171cd4 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/.project @@ -0,0 +1 @@ +Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\project2 diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/bin/python b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/bin/python new file mode 100644 index 000000000000..590cf8f553ef --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/bin/python @@ -0,0 +1 @@ +Not a real python binary diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/.project b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/.project new file mode 100644 index 000000000000..9b9083816e90 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/.project @@ -0,0 +1 @@ +Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\project3 \ No newline at end of file diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/.venv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/.venv/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/.venv/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/datascience-ui/data-explorer/emptyRowsView.css b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/parent/child/folder/dummyFile similarity index 100% rename from src/datascience-ui/data-explorer/emptyRowsView.css rename to src/test/pythonEnvironments/common/envlayouts/pipenv/project3/parent/child/folder/dummyFile diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/bin/python b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/bin/python new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/bin/python @@ -0,0 +1 @@ +Not real python exe diff --git a/src/datascience-ui/react-common/svgViewer.css b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi similarity index 100% rename from src/datascience-ui/react-common/svgViewer.css rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/datascience/notebook/empty.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi similarity index 100% rename from src/test/datascience/notebook/empty.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi diff --git a/src/test/pythonFiles/autoimport/one.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python similarity index 100% rename from src/test/pythonFiles/autoimport/one.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml new file mode 100644 index 000000000000..9b93e638e9ab --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml @@ -0,0 +1,14 @@ +[project] +name = "multi-env" +channels = ["conda-forge"] +platforms = ["win-64"] + +[feature.py310.dependencies] +python = "~=3.10" + +[feature.py311.dependencies] +python = "~=3.11" + +[environments] +py310 = ["py310"] +py311 = ["py311"] diff --git a/src/test/pythonFiles/autoimport/two/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python similarity index 100% rename from src/test/pythonFiles/autoimport/two/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python diff --git a/src/test/pythonFiles/autoimport/two/three.py b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi similarity index 100% rename from src/test/pythonFiles/autoimport/two/three.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml new file mode 100644 index 000000000000..f11ab3b42360 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml @@ -0,0 +1,11 @@ +[project] +name = "non-windows" +version = "0.1.0" +description = "Add a short description here" +authors = ["Bas Zalmstra <zalmstra.bas@gmail.com>"] +channels = ["conda-forge"] +platforms = ["win-64"] + +[tasks] + +[dependencies] diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml new file mode 100644 index 000000000000..1341496c5590 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml @@ -0,0 +1,12 @@ +[project] +name = "windows" +version = "0.1.0" +description = "Add a short description here" +authors = ["Bas Zalmstra <zalmstra.bas@gmail.com>"] +channels = ["conda-forge"] +platforms = ["win-64"] + +[tasks] + +[dependencies] +python = "~=3.8.0" diff --git a/src/test/pythonFiles/definition/navigation/__init__.py b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/activate similarity index 100% rename from src/test/pythonFiles/definition/navigation/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/activate diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/python.exe similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/python.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/python.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/pyvenv.cfg new file mode 100644 index 000000000000..8245a8f957a5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/pyvenv.cfg @@ -0,0 +1,3 @@ +home = ~\appdata\local\programs\python\python36 +include-system-site-packages = false +version = 3.6.1 diff --git a/src/test/pythonFiles/docstrings/one.py b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/activate similarity index 100% rename from src/test/pythonFiles/docstrings/one.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/activate diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/python.exe similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/python.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/python.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/pyvenv.cfg new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonFiles/environments/path1/one b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/activate similarity index 100% rename from src/test/pythonFiles/environments/path1/one rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/activate diff --git a/src/test/pythonFiles/environments/path1/one.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/python similarity index 100% rename from src/test/pythonFiles/environments/path1/one.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/python diff --git a/src/test/pythonFiles/environments/path1/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/pyvenv.cfg similarity index 100% rename from src/test/pythonFiles/environments/path1/python.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/pyvenv.cfg diff --git a/src/test/pythonFiles/environments/path2/one b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/activate similarity index 100% rename from src/test/pythonFiles/environments/path2/one rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/activate diff --git a/src/test/pythonFiles/environments/path2/one.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/python similarity index 100% rename from src/test/pythonFiles/environments/path2/one.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/python diff --git a/src/test/pythonFiles/environments/path2/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/pyvenv.cfg similarity index 100% rename from src/test/pythonFiles/environments/path2/python.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/pyvenv.cfg diff --git a/src/test/pythonFiles/folding/empty.py b/src/test/pythonEnvironments/common/envlayouts/poetry/project1-haha-py3.8/Scripts/python.exe similarity index 100% rename from src/test/pythonFiles/folding/empty.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/project1-haha-py3.8/Scripts/python.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv/pyvenv.cfg new file mode 100644 index 000000000000..0faad0624a4c --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.8.2.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (folder1) diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv2/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv2/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/.venv2/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project1/pyproject.toml b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/pyproject.toml new file mode 100644 index 000000000000..1afcc28bc13c --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project1/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "poetry-tutorial-project" +version = "0.1.0" +description = "" +authors = ["PVSC <pvscc@microsoft.com>"] + +[tool.poetry.dependencies] +python = "^3.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/bin/activate similarity index 100% rename from src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/bin/activate diff --git a/src/test/pythonFiles/testFiles/standard/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/bin/python similarity index 100% rename from src/test/pythonFiles/testFiles/standard/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/pyvenv.cfg new file mode 100644 index 000000000000..8245a8f957a5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/pyvenv.cfg @@ -0,0 +1,3 @@ +home = ~\appdata\local\programs\python\python36 +include-system-site-packages = false +version = 3.6.1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project2/pyproject.toml b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/pyproject.toml new file mode 100644 index 000000000000..1afcc28bc13c --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "poetry-tutorial-project" +version = "0.1.0" +description = "" +authors = ["PVSC <pvscc@microsoft.com>"] + +[tool.poetry.dependencies] +python = "^3.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project3/pyproject.toml b/src/test/pythonEnvironments/common/envlayouts/poetry/project3/pyproject.toml new file mode 100644 index 000000000000..884d85e9903e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project3/pyproject.toml @@ -0,0 +1,10 @@ +# This pyproject.toml has not been setup for poetry + +[tool.pipenv.dependencies] +python = "^3.5" + +[tool.pipenv.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project4/pyproject.toml b/src/test/pythonEnvironments/common/envlayouts/poetry/project4/pyproject.toml new file mode 100644 index 000000000000..627d86251d86 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project4/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetrzzzzy] +name = "poetry-tutorial-project" +version = "0.1.0" +description = "" + +[tool.poetry.dependencies] +python = "^3.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/activate b/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python3 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python37 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python37 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python37 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python38 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python38 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python38 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.7 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.7 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.7 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.8 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.8 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.8 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.9/empty b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.9/empty new file mode 100644 index 000000000000..0c6fe8957e8a --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.9/empty @@ -0,0 +1 @@ +this is intentionally empty diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/3.9.0/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/3.9.0/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python3.8 b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python3.8 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/conda-meta/history new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/conda-meta/history @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3 b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3.7 b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3.7 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/conda-meta/history new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/pyvenv.cfg new file mode 100644 index 000000000000..b8b1d803da5b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/pyvenv.cfg @@ -0,0 +1,3 @@ +home = ~/.pyenv/versions/3.9.0/bin +include-system-site-packages = false +version = 3.9.0 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/envlayouts/storeApps/Program Files/WindowsApps/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/venv1/python b/src/test/pythonEnvironments/common/envlayouts/venv1/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/venv1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/venv1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/venv2/bin/python b/src/test/pythonEnvironments/common/envlayouts/venv2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/venv2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/venv2/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenv1/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenv1/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenv2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualenv2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenv3/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualenv3/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/.project b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/.project new file mode 100644 index 000000000000..f16530171cd4 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/.project @@ -0,0 +1 @@ +Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\project2 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/bin/python new file mode 100644 index 000000000000..590cf8f553ef --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/bin/python @@ -0,0 +1 @@ +Not a real python binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/pyvenv.cfg new file mode 100644 index 000000000000..0faad0624a4c --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.8.2.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (folder1) diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3.8 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3.8 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3.8 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3.5 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3.5 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3.5 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3.5 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3.5 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3.5 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/conda-meta/history new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/conda-meta/history @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/py39/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/py39/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/python37/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/python37/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/python38/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/python38/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3 b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3.8 b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3.8 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3.8 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/pyvenv.cfg new file mode 100644 index 000000000000..8245a8f957a5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/pyvenv.cfg @@ -0,0 +1,3 @@ +home = ~\appdata\local\programs\python\python36 +include-system-site-packages = false +version = 3.6.1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/pyvenv.cfg new file mode 100644 index 000000000000..0faad0624a4c --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.8.2.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (folder1) diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/conda-meta/history new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/conda-meta/history @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/python b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix3custom/bin/python b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix3custom/bin/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix3custom/bin/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/pyvenv.cfg new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonEnvironments/common/posixUtils.unit.test.ts b/src/test/pythonEnvironments/common/posixUtils.unit.test.ts new file mode 100644 index 000000000000..6ff06f9bacba --- /dev/null +++ b/src/test/pythonEnvironments/common/posixUtils.unit.test.ts @@ -0,0 +1,122 @@ +// 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 { promises, Dirent } from 'fs'; +import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; +import { getPythonBinFromPosixPaths } from '../../../client/pythonEnvironments/common/posixUtils'; + +suite('Posix Utils tests', () => { + let readDirStub: sinon.SinonStub; + let resolveSymlinkStub: sinon.SinonStub; + + class FakeDirent extends Dirent { + constructor( + public readonly name: string, + private readonly _isFile: boolean, + private readonly _isLink: boolean, + ) { + super(); + } + + public isFile(): boolean { + return this._isFile; + } + + public isDirectory(): boolean { + return !this._isFile && !this._isLink; + } + + // eslint-disable-next-line class-methods-use-this + public isBlockDevice(): boolean { + return false; + } + + // eslint-disable-next-line class-methods-use-this + public isCharacterDevice(): boolean { + return false; + } + + public isSymbolicLink(): boolean { + return this._isLink; + } + + // eslint-disable-next-line class-methods-use-this + public isFIFO(): boolean { + return false; + } + + // eslint-disable-next-line class-methods-use-this + public isSocket(): boolean { + return false; + } + } + + setup(() => { + readDirStub = sinon.stub(promises, 'readdir'); + readDirStub + .withArgs(path.join('usr', 'bin'), { withFileTypes: true }) + .resolves([ + new FakeDirent('python', false, true), + new FakeDirent('python3', false, true), + new FakeDirent('python3.7', false, true), + new FakeDirent('python3.8', false, true), + ]); + readDirStub + .withArgs(path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib'), { + withFileTypes: true, + }) + .resolves([new FakeDirent('python3.9', true, false)]); + + resolveSymlinkStub = sinon.stub(externalDependencies, 'resolveSymbolicLink'); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python3.7')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.7', 'lib', 'python3.7'), + ); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python3')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.7', 'lib', 'python3.7'), + ); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.7', 'lib', 'python3.7'), + ); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python3.8')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.8', 'lib', 'python3.8'), + ); + resolveSymlinkStub + .withArgs( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib', 'python3.9'), + ) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib', 'python3.9'), + ); + }); + + teardown(() => { + readDirStub.restore(); + resolveSymlinkStub.restore(); + }); + test('getPythonBinFromPosixPaths', async () => { + const expectedPaths = [ + path.join('usr', 'bin', 'python'), + path.join('usr', 'bin', 'python3.8'), + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib', 'python3.9'), + ].sort((a, b) => a.length - b.length); + + const actualPaths = await getPythonBinFromPosixPaths([ + path.join('usr', 'bin'), + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib'), + ]); + actualPaths.sort((a, b) => a.length - b.length); + + assert.deepStrictEqual(actualPaths, expectedPaths); + }); +}); diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case1 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case1 new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case1 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case2 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case2 new file mode 100644 index 000000000000..d5bab55214ac --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case2 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0a1-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case3 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case3 new file mode 100644 index 000000000000..4de9c7e72768 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case3 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0b2-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case4 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case4 new file mode 100644 index 000000000000..f6d8f0b9c027 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case4 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0rc1-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case5 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case5 new file mode 100644 index 000000000000..5b5be621a41e --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case5 @@ -0,0 +1,46 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0b2-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] +==> 2020-10-31 17:17:40 <== +# cmd: /home/kanadig/.pyenv/versions/miniconda3-4.7.12/bin/conda update python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0rc2-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case1 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case1 new file mode 100644 index 000000000000..0f664d82a0d5 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case1 @@ -0,0 +1,3 @@ +home = /home/kanadig/.pyenv/versions/3.9.0/bin +include-system-site-packages = false +version = 3.9.0 diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case2 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case2 new file mode 100644 index 000000000000..706bcf757bd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case2 @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.8.2.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case3 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case3 new file mode 100644 index 000000000000..58c87730bf79 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case3 @@ -0,0 +1,3 @@ +home = /home/kanadig/.pyenv/versions/3.9.0/bin +include-system-site-packages = false +version = 3.9.0rc1 diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case4 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case4 new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case4 @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) 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..96b2f9c68244 --- /dev/null +++ b/src/test/pythonEnvironments/common/windowsUtils.unit.test.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { matchPythonBinFilename } 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(matchPythonBinFilename(testParam.path), testParam.expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts new file mode 100644 index 000000000000..2900b9b89c8f --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, TextDocument, Range, Uri, WorkspaceConfiguration, ConfigurationScope } from 'vscode'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { SpawnOptions } from '../../../../client/common/process/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +chaiUse(chaiAsPromised.default); + +function getSomeRequirementFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +const MISSING_PACKAGES_STR = + '[{"line": 8, "character": 34, "endLine": 8, "endCharacter": 44, "package": "flake8-csv", "code": "not-installed", "severity": 3}]'; +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +suite('Install check diagnostics tests', () => { + let plainExecStub: sinon.SinonStub; + let interpreterService: typemoq.IMock<IInterpreterService>; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock<WorkspaceConfiguration>; + + setup(() => { + configMock = typemoq.Mock.ofType<WorkspaceConfiguration>(); + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test parse diagnostics', async () => { + configMock + .setup((c) => c.get<string>('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); + plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, MISSING_PACKAGES); + configMock.verifyAll(); + }); + + test('Test parse empty diagnostics', async () => { + configMock + .setup((c) => c.get<string>('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); + plainExecStub.resolves({ stdout: '', stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, []); + configMock.verifyAll(); + }); + + [ + ['Error', '0'], + ['Warning', '1'], + ['Information', '2'], + ['Hint', '3'], + ].forEach((severityType: string[]) => { + const setting = severityType[0]; + const expected = severityType[1]; + test(`Test missing package severity: ${setting}`, async () => { + configMock + .setup((c) => c.get<string>('missingPackage.severity', 'Hint')) + .returns(() => setting) + .verifiable(typemoq.Times.atLeastOnce()); + let severity: string | undefined; + plainExecStub.callsFake((_cmd: string, _args: string[], options: SpawnOptions) => { + severity = options.env?.VSCODE_MISSING_PGK_SEVERITY; + return { stdout: '', stderr: '' }; + }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, []); + assert.deepStrictEqual(severity, expected); + configMock.verifyAll(); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts new file mode 100644 index 000000000000..1d3df521fd0a --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { assert, expect, use as chaiUse } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +// import * as typemoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { pickWorkspaceFolder } from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; + +chaiUse(chaiAsPromised.default); + +suite('Create environment workspace selection tests', () => { + let showQuickPickWithBackStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + + setup(() => { + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No workspaces (undefined)', async () => { + getWorkspaceFoldersStub.returns(undefined); + assert.isUndefined(await pickWorkspaceFolder()); + assert.isTrue(showErrorMessageStub.calledOnce); + }); + + test('No workspaces (empty array)', async () => { + getWorkspaceFoldersStub.returns([]); + assert.isUndefined(await pickWorkspaceFolder()); + assert.isTrue(showErrorMessageStub.calledOnce); + }); + + test('User did not select workspace or user hit escape', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file('some_folder'), + name: 'some_folder', + index: 0, + }, + { + uri: Uri.file('some_folder2'), + name: 'some_folder2', + index: 1, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns(undefined); + assert.isUndefined(await pickWorkspaceFolder()); + }); + + test('User clicked on the back button', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file('some_folder'), + name: 'some_folder', + index: 0, + }, + { + uri: Uri.file('some_folder2'), + name: 'some_folder2', + index: 1, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.throws(windowApis.MultiStepAction.Back); + expect(pickWorkspaceFolder()).to.eventually.be.rejectedWith(windowApis.MultiStepAction.Back); + }); + + test('single workspace scenario', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns({ + label: workspaces[0].name, + detail: workspaces[0].uri.fsPath, + description: undefined, + }); + + const workspace = await pickWorkspaceFolder(); + assert.deepEqual(workspace, workspaces[0]); + assert(showQuickPickWithBackStub.notCalled); + }); + + test('Multi-workspace scenario with single workspace selected', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace2')), + name: 'workspace2', + index: 1, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace3')), + name: 'workspace3', + index: 2, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace4')), + name: 'workspace4', + index: 3, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace5')), + name: 'workspace5', + index: 4, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns({ + label: workspaces[1].name, + detail: workspaces[1].uri.fsPath, + description: undefined, + }); + + const workspace = await pickWorkspaceFolder(); + assert.deepEqual(workspace, workspaces[1]); + assert(showQuickPickWithBackStub.calledOnce); + }); + + test('Multi-workspace scenario with multiple workspaces selected', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace2')), + name: 'workspace2', + index: 1, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace3')), + name: 'workspace3', + index: 2, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace4')), + name: 'workspace4', + index: 3, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace5')), + name: 'workspace5', + index: 4, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns([ + { + label: workspaces[1].name, + detail: workspaces[1].uri.fsPath, + description: undefined, + }, + { + label: workspaces[3].name, + detail: workspaces[3].uri.fsPath, + description: undefined, + }, + ]); + + const workspace = await pickWorkspaceFolder({ allowMultiSelect: true }); + assert.deepEqual(workspace, [workspaces[1], workspaces[3]]); + assert(showQuickPickWithBackStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts new file mode 100644 index 000000000000..dd09203d65cc --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { + IInterpreterQuickPick, + IPythonPathUpdaterServiceManager, +} from '../../../client/interpreter/configuration/types'; +import { registerCreateEnvironmentFeatures } from '../../../client/pythonEnvironments/creation/createEnvApi'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('Create Environment APIs', () => { + let registerCommandStub: sinon.SinonStub; + let showQuickPickStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let interpreterQuickPick: typemoq.IMock<IInterpreterQuickPick>; + let interpreterPathService: typemoq.IMock<IPythonPathUpdaterServiceManager>; + let pathUtils: typemoq.IMock<IPathUtils>; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + registerCommandStub = sinon.stub(commandApis, 'registerCommand'); + interpreterQuickPick = typemoq.Mock.ofType<IInterpreterQuickPick>(); + interpreterPathService = typemoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + pathUtils = typemoq.Mock.ofType<IPathUtils>(); + + registerCommandStub.callsFake((_command: string, _callback: (...args: any[]) => any) => ({ + dispose: () => { + // Do nothing + }, + })); + + pathUtils.setup((p) => p.getDisplayName(typemoq.It.isAny())).returns(() => 'test'); + + registerCreateEnvironmentFeatures( + disposables, + interpreterQuickPick.object, + interpreterPathService.object, + pathUtils.object, + ); + }); + teardown(() => { + disposables.forEach((d) => d.dispose()); + sinon.restore(); + }); + + [true, false].forEach((selectEnvironment) => { + test(`Set environment selectEnvironment == ${selectEnvironment}`, async () => { + const workspace1 = { + uri: Uri.file('/path/to/env'), + name: 'workspace1', + index: 0, + }; + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider + .setup((p) => p.createEnvironment(typemoq.It.isAny())) + .returns(() => + Promise.resolve({ + path: '/path/to/env', + workspaceFolder: workspace1, + action: undefined, + error: undefined, + }), + ); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + + interpreterPathService + .setup((p) => + p.updatePythonPath( + typemoq.It.isValue('/path/to/env'), + ConfigurationTarget.WorkspaceFolder, + 'ui', + typemoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(selectEnvironment ? typemoq.Times.once() : typemoq.Times.never()); + + await handleCreateEnvironmentCommand([provider.object], { selectEnvironment }); + + assert.ok(showQuickPickStub.calledOnce); + assert.ok(selectEnvironment ? showInformationMessageStub.calledOnce : showInformationMessageStub.notCalled); + interpreterPathService.verifyAll(); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts new file mode 100644 index 000000000000..b666191b37bf --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { WorkspaceConfiguration } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerCreateEnvironmentButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let onDidChangeConfigurationStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock<WorkspaceConfiguration>; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); + onDidChangeConfigurationStub.returns(new FakeDisposable()); + + configMock = typemoq.Mock.ofType<WorkspaceConfiguration>(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + getConfigurationStub.returns(configMock.object); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('python.createEnvironment.contentButton setting is set to "show", no files open', async () => { + registerCreateEnvironmentButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting is set to "hide", no files open', async () => { + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + + registerCreateEnvironmentButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); + + test('python.createEnvironment.contentButton setting changed from "hide" to "show"', async () => { + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvironmentButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting changed from "show" to "hide"', async () => { + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvironmentButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts new file mode 100644 index 000000000000..9aa9a606d22f --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { onCreateEnvironmentStarted } from '../../../client/pythonEnvironments/creation/createEnvApi'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('Create Environments Tests', () => { + let showQuickPickStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let startedEventTriggered = false; + let exitedEventTriggered = false; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + startedEventTriggered = false; + exitedEventTriggered = false; + disposables.push( + onCreateEnvironmentStarted(() => { + startedEventTriggered = true; + }), + ); + disposables.push( + onCreateEnvironmentStarted(() => { + exitedEventTriggered = true; + }), + ); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Successful environment creation', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + assert.isTrue(showQuickPickWithBackStub.notCalled); + provider.verifyAll(); + }); + + test('Successful environment creation with Back', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickWithBackStub.resolves(provider.object); + + await handleCreateEnvironmentCommand([provider.object], { showBackButton: true }); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + assert.isTrue(showQuickPickStub.notCalled); + provider.verifyAll(); + }); + + test('Environment creation error', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.reject(new Error('test'))); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + await assert.isRejected(handleCreateEnvironmentCommand([provider.object])); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + provider.verifyAll(); + }); + + test('Environment creation error with Back', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.reject(new Error('test'))); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickWithBackStub.resolves(provider.object); + await assert.isRejected(handleCreateEnvironmentCommand([provider.object], { showBackButton: true })); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + provider.verifyAll(); + }); + + test('No providers registered', async () => { + await handleCreateEnvironmentCommand([]); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.isFalse(startedEventTriggered); + assert.isFalse(exitedEventTriggered); + }); + + test('Single environment creation provider registered', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(showQuickPickStub.calledOnce); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Multiple environment creation providers registered', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickStub.resolves({ + id: 'test-id2', + label: 'test2', + description: 'test-description2', + }); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + await handleCreateEnvironmentCommand([provider1.object, provider2.object]); + + assert.isTrue(showQuickPickStub.calledOnce); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Single environment creation provider registered with Back', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickWithBackStub.resolves(provider.object); + await handleCreateEnvironmentCommand([provider.object], { showBackButton: true }); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Multiple environment creation providers registered with Back', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickWithBackStub.resolves({ + id: 'test-id2', + label: 'test2', + description: 'test-description2', + }); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + await handleCreateEnvironmentCommand([provider1.object, provider2.object], { showBackButton: true }); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('User clicked Back', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickWithBackStub.returns(Promise.reject(windowApis.MultiStepAction.Back)); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + const result = await handleCreateEnvironmentCommand([provider1.object, provider2.object], { + showBackButton: true, + }); + + assert.deepStrictEqual(result, { + action: 'Back', + workspaceFolder: undefined, + path: undefined, + error: undefined, + }); + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + }); + + test('User pressed Escape', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickWithBackStub.returns(Promise.reject(windowApis.MultiStepAction.Cancel)); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + const result = await handleCreateEnvironmentCommand([provider1.object, provider2.object], { + showBackButton: true, + }); + + assert.deepStrictEqual(result, { + action: 'Cancel', + workspaceFolder: undefined, + path: undefined, + error: undefined, + }); + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts new file mode 100644 index 000000000000..d4041ef4bb88 --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as commonUtils from '../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheck, +} from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { Commands } from '../../../client/common/constants'; +import { Common, CreateEnv } from '../../../client/common/utils/localize'; + +suite('Create Environment Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let hasVenvStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let hasRequirementFilesStub: sinon.SinonStub; + let hasKnownFilesStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + let isCreateEnvWorkspaceCheckNotRunStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + hasVenvStub = sinon.stub(commonUtils, 'hasVenv'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + hasRequirementFilesStub = sinon.stub(triggerUtils, 'hasRequirementFiles'); + hasKnownFilesStub = sinon.stub(triggerUtils, 'hasKnownFiles'); + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + isCreateEnvWorkspaceCheckNotRunStub = sinon.stub(triggerUtils, 'isCreateEnvWorkspaceCheckNotRun'); + isCreateEnvWorkspaceCheckNotRunStub.returns(true); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No Uri', async () => { + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, undefined); + sinon.assert.notCalled(shouldPromptToCreateEnvStub); + }); + + test('Should not perform checks if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not perform checks even if force is true, if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri, { + force: true, + }); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".venv"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(true); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".conda"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(true); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are no requirements', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are known files', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(true); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if selected python is not global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should show prompt if all conditions met: User closes prompt', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + showInformationMessageStub.resolves(undefined); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks create', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.calledOnceWithExactly(executeCommandStub, Commands.Create_Environment); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + }); + + test("Should show prompt if all conditions met: User clicks don't show again", async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(Common.doNotShowAgain); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts new file mode 100644 index 000000000000..2b6a8df91d82 --- /dev/null +++ b/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { assert } from 'chai'; +import * as typemoq from 'typemoq'; +import { + Disposable, + Terminal, + TerminalShellExecution, + TerminalShellExecutionStartEvent, + TerminalShellIntegration, + Uri, +} from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { registerTriggerForPipInTerminal } from '../../../client/pythonEnvironments/creation/globalPipInTerminalTrigger'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { Common, CreateEnv } from '../../../client/common/utils/localize'; + +suite('Global Pip in Terminal Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showWarningMessageStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + let onDidStartTerminalShellExecutionStub: sinon.SinonStub; + let handler: undefined | ((e: TerminalShellExecutionStartEvent) => Promise<void>); + let execEvent: typemoq.IMock<TerminalShellExecutionStartEvent>; + let shellIntegration: typemoq.IMock<TerminalShellIntegration>; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + const outsideWorkspace = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'outsideWorkspace'), + ); + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([workspace1]); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showWarningMessageStub = sinon.stub(windowApis, 'showWarningMessage'); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + executeCommandStub.resolves({ path: 'some/python' }); + + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + + onDidStartTerminalShellExecutionStub = sinon.stub(windowApis, 'onDidStartTerminalShellExecution'); + onDidStartTerminalShellExecutionStub.callsFake((cb) => { + handler = cb; + return { + dispose: () => { + handler = undefined; + }, + }; + }); + + shellIntegration = typemoq.Mock.ofType<TerminalShellIntegration>(); + execEvent = typemoq.Mock.ofType<TerminalShellExecutionStartEvent>(); + execEvent.setup((e) => e.shellIntegration).returns(() => shellIntegration.object); + shellIntegration + .setup((s) => s.executeCommand(typemoq.It.isAnyString())) + .returns(() => (({} as unknown) as TerminalShellExecution)); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Should not prompt to create environment if setting is off', async () => { + shouldPromptToCreateEnvStub.returns(false); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + assert.strictEqual(disposables.length, 0); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + }); + + test('Should not prompt to create environment if no workspace folders', async () => { + shouldPromptToCreateEnvStub.returns(true); + getWorkspaceFoldersStub.returns([]); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + assert.strictEqual(disposables.length, 0); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFoldersStub); + }); + + test('Should not prompt to create environment if workspace folder is not found', async () => { + shouldPromptToCreateEnvStub.returns(true); + getWorkspaceFolderStub.returns(undefined); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + shellIntegration.setup((s) => s.cwd).returns(() => outsideWorkspace); + await handler?.(({ shellIntegration: shellIntegration.object } as unknown) as TerminalShellExecutionStartEvent); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if global python is not selected', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(false); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.(({ shellIntegration: shellIntegration.object } as unknown) as TerminalShellExecutionStartEvent); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if command is not trusted', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: false, + value: 'pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if command does not start with pip install', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: false, + value: 'some command pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + ['pip install', 'pip3 install', 'python -m pip install', 'python3 -m pip install'].forEach((command) => { + test(`Should prompt to create environment if all conditions are met: ${command}`, async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + showWarningMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: true, + value: command, + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve(command); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showWarningMessageStub); + sinon.assert.calledOnce(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + + shellIntegration.verify((s) => s.executeCommand(typemoq.It.isAnyString()), typemoq.Times.once()); + }); + }); + + test("Should disable create environment trigger if user selects don't show again", async () => { + shouldPromptToCreateEnvStub.returns(true); + + isGlobalPythonSelectedStub.returns(true); + showWarningMessageStub.resolves(Common.doNotShowAgain); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: true, + value: 'pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showWarningMessageStub); + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts new file mode 100644 index 000000000000..21bddd33c678 --- /dev/null +++ b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, DiagnosticCollection, TextEditor, Range, Uri, TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as languageApis from '../../../client/common/vscodeApis/languageApis'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import * as installUtils from '../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import { + DEPS_NOT_INSTALLED_KEY, + registerInstalledPackagesDiagnosticsProvider, +} from '../../../client/pythonEnvironments/creation/installedPackagesDiagnostic'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +function getSomeFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +function getSomeRequirementFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +function getPyProjectTomlFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'pyproject.toml'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +function getSomeTomlFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'something.toml'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + let onDidCloseTextDocumentStub: sinon.SinonStub; + let onDidChangeDiagnosticsStub: sinon.SinonStub; + let onDidChangeActiveTextEditorStub: sinon.SinonStub; + let createDiagnosticCollectionStub: sinon.SinonStub; + let diagnosticCollection: typemoq.IMock<DiagnosticCollection>; + let getActiveTextEditorStub: sinon.SinonStub; + let textEditor: typemoq.IMock<TextEditor>; + let getInstalledPackagesDiagnosticsStub: sinon.SinonStub; + let interpreterService: typemoq.IMock<IInterpreterService>; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + getOpenTextDocumentsStub.returns([]); + + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + onDidCloseTextDocumentStub = sinon.stub(workspaceApis, 'onDidCloseTextDocument'); + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + onDidCloseTextDocumentStub.returns(new FakeDisposable()); + + onDidChangeDiagnosticsStub = sinon.stub(languageApis, 'onDidChangeDiagnostics'); + onDidChangeDiagnosticsStub.returns(new FakeDisposable()); + createDiagnosticCollectionStub = sinon.stub(languageApis, 'createDiagnosticCollection'); + diagnosticCollection = typemoq.Mock.ofType<DiagnosticCollection>(); + diagnosticCollection.setup((d) => d.set(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.clear()).returns(() => undefined); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.has(typemoq.It.isAny())).returns(() => false); + createDiagnosticCollectionStub.returns(diagnosticCollection.object); + + onDidChangeActiveTextEditorStub = sinon.stub(windowApis, 'onDidChangeActiveTextEditor'); + onDidChangeActiveTextEditorStub.returns(new FakeDisposable()); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + textEditor = typemoq.Mock.ofType<TextEditor>(); + getActiveTextEditorStub.returns(textEditor.object); + + getInstalledPackagesDiagnosticsStub = sinon.stub(installUtils, 'getInstalledPackagesDiagnostics'); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.onDidChangeInterpreter(typemoq.It.isAny(), undefined, undefined)) + .returns(() => new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Ensure nothing is run if there are no open documents', () => { + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should not run packages check if opened files are not dep files', () => { + const someFile = getSomeFile(); + const someTomlFile = getSomeTomlFile(); + getOpenTextDocumentsStub.returns([someFile.object, someTomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should run packages check if opened files are dep files', () => { + const reqFile = getSomeRequirementFile(); + const tomlFile = getPyProjectTomlFile(); + getOpenTextDocumentsStub.returns([reqFile.object, tomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(getInstalledPackagesDiagnosticsStub.calledTwice); + }); + + [getSomeRequirementFile().object, getPyProjectTomlFile().object].forEach((file) => { + test(`Should run packages check on open of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on save of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on close of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidCloseTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + diagnosticCollection.reset(); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + diagnosticCollection + .setup((d) => d.has(typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + handler(file); + diagnosticCollection.verifyAll(); + }); + + test(`Should trigger a context update on active editor switch to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + + test(`Should trigger a context update to true on diagnostic change to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + }); + + [getSomeFile().object, getSomeTomlFile().object].forEach((file) => { + test(`Should not run packages check on open of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should not run packages check on save of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should trigger a context update on active editor switch to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + + test(`Should trigger a context update to false on diagnostic change to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts new file mode 100644 index 000000000000..ee177a58c779 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import { hasVenv } from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; + +suite('CommonUtils', () => { + let fileExistsStub: sinon.SinonStub; + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + fileExistsStub = sinon.stub(fs, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Venv exists test', async () => { + fileExistsStub.resolves(true); + const result = await hasVenv(workspace1); + expect(result).to.be.equal(true, 'Incorrect result'); + + fileExistsStub.calledOnceWith(path.join(workspace1.uri.fsPath, '.venv', 'pyvenv.cfg')); + }); + + test('Venv does not exist test', async () => { + fileExistsStub.resolves(false); + const result = await hasVenv(workspace1); + expect(result).to.be.equal(false, 'Incorrect result'); + + fileExistsStub.calledOnceWith(path.join(workspace1.uri.fsPath, '.venv', 'pyvenv.cfg')); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts new file mode 100644 index 000000000000..e2ff9b2ab486 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { assert, use as chaiUse } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import { condaCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; +import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import * as condaUtils from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { Output } from '../../../../client/common/process/types'; +import { createDeferred } from '../../../../client/common/utils/async'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { CONDA_ENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/condaProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; +import { + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('Conda Creation provider tests', () => { + let condaProvider: CreateEnvironmentProvider; + let progressMock: typemoq.IMock<CreateEnvironmentProgress>; + let getCondaBaseEnvStub: sinon.SinonStub; + let pickPythonVersionStub: sinon.SinonStub; + let pickWorkspaceFolderStub: sinon.SinonStub; + let execObservableStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickExistingCondaActionStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + + setup(() => { + pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); + getCondaBaseEnvStub = sinon.stub(condaUtils, 'getCondaBaseEnv'); + pickPythonVersionStub = sinon.stub(condaUtils, 'pickPythonVersion'); + execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + + progressMock = typemoq.Mock.ofType<CreateEnvironmentProgress>(); + condaProvider = condaCreationProvider(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No conda installed', async () => { + getCondaBaseEnvStub.resolves(undefined); + + assert.isUndefined(await condaProvider.createEnvironment()); + }); + + test('No workspace selected', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves(undefined); + + await assert.isRejected(condaProvider.createEnvironment()); + }); + + test('No python version picked selected', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + pickPythonVersionStub.resolves(undefined); + + await assert.isRejected(condaProvider.createEnvironment()); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Create conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + assert.deepStrictEqual(await promise, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Create conda environment failed', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _error: undefined | ((error: unknown) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: undefined, + out: { + subscribe: ( + _next?: (value: Output<string>) => void, + // eslint-disable-next-line no-shadow + error?: (error: unknown) => void, + complete?: () => void, + ) => { + _error = error; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_error); + _error!('bad arguments'); + _complete!(); + const result = await promise; + assert.ok(result?.error); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Create conda environment failed (non-zero exit code)', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + const result = await promise; + assert.ok(result?.error); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Use existing conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.UseExisting); + getPrefixCondaEnvPathStub.returns('existing_environment'); + + const result = await condaProvider.createEnvironment(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickPythonVersionStub.notCalled); + assert.isTrue(execObservableStub.notCalled); + assert.isTrue(withProgressStub.notCalled); + + assert.deepStrictEqual(result, { path: 'existing_environment', workspaceFolder: workspace1 }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..b1acd0678714 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { deleteCondaEnvironment } from '../../../../client/pythonEnvironments/creation/provider/condaDeleteUtils'; + +suite('Conda Delete test', () => { + let plainExecStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete conda env ', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isTrue(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete conda env with error', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(true); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete conda env with exception', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.rejects(new Error('error')); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts new file mode 100644 index 000000000000..a3f4a1abe905 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { + ExistingCondaAction, + pickExistingCondaAction, + pickPythonVersion, +} from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; + +suite('Conda Utils test', () => { + let showQuickPickWithBackStub: sinon.SinonStub; + + setup(() => { + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No version selected or user pressed escape', async () => { + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickPythonVersion(); + assert.isUndefined(actual); + }); + + test('User selected a version', async () => { + showQuickPickWithBackStub.resolves({ label: 'Python', description: '3.10' }); + + const actual = await pickPythonVersion(); + assert.equal(actual, '3.10'); + }); + + test('With cancellation', async () => { + const source = new CancellationTokenSource(); + + showQuickPickWithBackStub.callsFake(() => { + source.cancel(); + }); + + const actual = await pickPythonVersion(source.token); + assert.isUndefined(actual); + }); +}); + +suite('Existing .conda env test', () => { + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No .conda found', async () => { + hasPrefixCondaEnvStub.resolves(false); + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Create); + assert.isTrue(showQuickPickWithBackStub.notCalled); + }); + + test('User presses escape', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingCondaAction(workspace1)); + }); + + test('.conda found and user selected to re-create', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.recreate, + description: CreateEnv.Conda.recreateDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Recreate); + }); + + test('.conda found and user selected to re-use', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.UseExisting); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts new file mode 100644 index 000000000000..aa2d317c405e --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -0,0 +1,551 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import * as sinon from 'sinon'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import { VenvCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; +import { IInterpreterQuickPick } from '../../../../client/interpreter/configuration/types'; +import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { createDeferred } from '../../../../client/common/utils/async'; +import { Output, SpawnOptions } from '../../../../client/common/process/types'; +import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; +import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('venv Creation provider tests', () => { + let venvProvider: CreateEnvironmentProvider; + let pickWorkspaceFolderStub: sinon.SinonStub; + let interpreterQuickPick: typemoq.IMock<IInterpreterQuickPick>; + let progressMock: typemoq.IMock<CreateEnvironmentProgress>; + let execObservableStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickPackagesToInstallStub: sinon.SinonStub; + let pickExistingVenvActionStub: sinon.SinonStub; + let deleteEnvironmentStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pickExistingVenvActionStub = sinon.stub(venvUtils, 'pickExistingVenvAction'); + deleteEnvironmentStub = sinon.stub(venvUtils, 'deleteEnvironment'); + pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); + execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + interpreterQuickPick = typemoq.Mock.ofType<IInterpreterQuickPick>(); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + pickPackagesToInstallStub = sinon.stub(venvUtils, 'pickPackagesToInstall'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + progressMock = typemoq.Mock.ofType<CreateEnvironmentProgress>(); + venvProvider = new VenvCreationProvider(interpreterQuickPick.object); + + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Create); + deleteEnvironmentStub.resolves(true); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No workspace selected', async () => { + pickWorkspaceFolderStub.resolves(undefined); + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await assert.isRejected(venvProvider.createEnvironment()); + assert.isTrue(pickWorkspaceFolderStub.calledOnce); + interpreterQuickPick.verifyAll(); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(pickExistingVenvActionStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('No Python selected', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + await assert.isRejected(venvProvider.createEnvironment()); + + assert.isTrue(pickWorkspaceFolderStub.calledOnce); + interpreterQuickPick.verifyAll(); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('User pressed Esc while selecting dependencies', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves(undefined); + + await assert.isRejected(venvProvider.createEnvironment()); + assert.isTrue(pickPackagesToInstallStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with python selected by user no packages selected', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv failed', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _error: undefined | ((error: unknown) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + _next?: (value: Output<string>) => void, + // eslint-disable-next-line no-shadow + error?: (error: unknown) => void, + complete?: () => void, + ) => { + _error = error; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_error); + _error!('bad arguments'); + _complete!(); + const result = await promise; + assert.ok(result?.error); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv failed (non-zero exit code)', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + const result = await promise; + assert.ok(result?.error); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with pre-existing .venv, user selects re-create', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects re-create, delete env failed', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + deleteEnvironmentStub.resolves(false); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + await assert.isRejected(venvProvider.createEnvironment()); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects use existing', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.UseExisting); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.never()); + + pickPackagesToInstallStub.resolves([]); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with 1000 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 1000 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) }); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + execObservableStub.callsFake((_c, argv: string[], options) => { + stdin = options?.stdinStr; + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.strictEqual(stdin, expected); + assert.isTrue(hasStdinArg); + }); + + test('Create venv with 5 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 5 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expectedRequirements = requirements.map((r) => r.installItem).sort(); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + let actualRequirements: string[] = []; + execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => { + stdin = options?.stdinStr; + actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort(); + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.isUndefined(stdin); + assert.deepStrictEqual(actualRequirements, expectedRequirements); + assert.isFalse(hasStdinArg); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..231222acbaec --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { assert } from 'chai'; +import * as path from 'path'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { + deleteEnvironmentNonWindows, + deleteEnvironmentWindows, +} from '../../../../client/pythonEnvironments/creation/provider/venvDeleteUtils'; +import * as switchPython from '../../../../client/pythonEnvironments/creation/provider/venvSwitchPython'; +import * as asyncApi from '../../../../client/common/utils/async'; + +suite('Test Delete environments (windows)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let unlinkStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let switchPythonStub: sinon.SinonStub; + let sleepStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + pathExistsStub.resolves(true); + + rmdirStub = sinon.stub(fs, 'rmdir'); + unlinkStub = sinon.stub(fs, 'unlink'); + + sleepStub = sinon.stub(asyncApi, 'sleep'); + sleepStub.resolves(); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + switchPythonStub = sinon.stub(switchPython, 'switchSelectedPython'); + switchPythonStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + rmdirStub.resolves(); + unlinkStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe succeeded but venv dir failed', async () => { + rmdirStub.rejects(); + unlinkStub.resolves(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed first attempt', async () => { + unlinkStub.rejects(); + rmdirStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe failed all attempts', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed no interpreter', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, undefined)); + assert.ok(switchPythonStub.notCalled); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); + +suite('Test Delete environments (linux/mac)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + rmdirStub = sinon.stub(fs, 'rmdir'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + pathExistsStub.resolves(true); + rmdirStub.resolves(); + + assert.ok(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete venv folder failed', async () => { + pathExistsStub.resolves(true); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts new file mode 100644 index 000000000000..ecb7d1434ada --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { + VENV_CREATED_MARKER, + VenvProgressAndTelemetry, +} from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import * as telemetry from '../../../../client/telemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; + +suite('Venv Progress and Telemetry', () => { + let sendTelemetryEventStub: sinon.SinonStub; + let progressReporterMock: typemoq.IMock<CreateEnvironmentProgress>; + + setup(() => { + sendTelemetryEventStub = sinon.stub(telemetry, 'sendTelemetryEvent'); + progressReporterMock = typemoq.Mock.ofType<CreateEnvironmentProgress>(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure telemetry event and progress are sent', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); + + test('Do not trigger telemetry event the second time', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts new file mode 100644 index 000000000000..2c8ec2ebce87 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -0,0 +1,489 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert, use as chaiUse } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as path from 'path'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { + ExistingVenvAction, + OPEN_REQUIREMENTS_BUTTON, + pickExistingVenvAction, + pickPackagesToInstall, +} from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; +import { createDeferred } from '../../../../client/common/utils/async'; + +chaiUse(chaiAsPromised.default); + +suite('Venv Utils test', () => { + let findFilesStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let showTextDocumentStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + findFilesStub = sinon.stub(workspaceApis, 'findFiles'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No requirements or toml found', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(false); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, []); + }); + + test('Toml found with no build system', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves('[project]\nname = "spam"\nversion = "2020.0.0"\n'); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, []); + }); + + test('Toml found with no project table', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[tool.poetry]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]', + ); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, []); + }); + + test('Toml found with no optional deps', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]', + ); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Toml found with deps, but user presses escape', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickWithBackStub.resolves(undefined); + + await assert.isRejected(pickPackagesToInstall(workspace1)); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + }); + + test('Toml found with dependencies and user selects None', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickWithBackStub.resolves([]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Toml found with dependencies and user selects One', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickWithBackStub.resolves([{ label: 'doc' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + installItem: 'doc', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Toml found with dependencies and user selects Few', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]\ncov = ["pytest-cov"]', + ); + + showQuickPickWithBackStub.resolves([{ label: 'test' }, { label: 'cov' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }, { label: 'cov' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + installItem: 'test', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + { + installType: 'toml', + installItem: 'cov', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Requirements found, but user presses escape', async () => { + pathExistsStub.resolves(true); + readFileStub.resolves('[project]\nname = "spam"\nversion = "2020.0.0"\n'); + + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + + showQuickPickWithBackStub.resolves(undefined); + + await assert.isRejected(pickPackagesToInstall(workspace1)); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.isTrue(readFileStub.calledOnce); + assert.isTrue(pathExistsStub.calledOnce); + }); + + test('Requirements found and user selects None', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickWithBackStub.resolves([]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.deepStrictEqual(actual, []); + assert.isTrue(readFileStub.notCalled); + }); + + test('Requirements found and user selects One', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickWithBackStub.resolves([{ label: 'requirements.txt' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'requirements', + installItem: path.join(workspace1.uri.fsPath, 'requirements.txt'), + }, + ]); + assert.isTrue(readFileStub.notCalled); + }); + + test('Requirements found and user selects Few', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickWithBackStub.resolves([{ label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'requirements', + installItem: path.join(workspace1.uri.fsPath, 'dev-requirements.txt'), + }, + { + installType: 'requirements', + installItem: path.join(workspace1.uri.fsPath, 'test-requirements.txt'), + }, + ]); + assert.isTrue(readFileStub.notCalled); + }); + + test('User clicks button to open requirements.txt', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + const deferred = createDeferred(); + showQuickPickWithBackStub.callsFake(async (_items, _options, _token, callback) => { + callback({ + button: OPEN_REQUIREMENTS_BUTTON, + item: { label: 'requirements.txt' }, + }); + await deferred.promise; + return [{ label: 'requirements.txt' }]; + }); + + let uri: Uri | undefined; + showTextDocumentStub.callsFake((arg: Uri) => { + uri = arg; + deferred.resolve(); + return Promise.resolve(); + }); + + await pickPackagesToInstall(workspace1); + assert.deepStrictEqual( + uri?.toString(), + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')).toString(), + ); + }); +}); + +suite('Test pick existing venv action', () => { + let withProgressStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + teardown(() => { + sinon.restore(); + }); + + test('User selects existing venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.UseExisting); + }); + + test('User presses escape', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('User selects delete venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }); + withProgressStub.resolves(true); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Recreate); + }); + + test('User clicks on back', async () => { + pathExistsStub.resolves(true); + // We use reject with "Back" to simulate the user clicking on back. + showQuickPickWithBackStub.rejects(windowApis.MultiStepAction.Back); + withProgressStub.resolves(false); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('No venv found', async () => { + pathExistsStub.resolves(false); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Create); + }); +}); diff --git a/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts new file mode 100644 index 000000000000..3e787570304a --- /dev/null +++ b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerPyProjectTomlFeatures } from '../../../client/pythonEnvironments/creation/pyProjectTomlContext'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +function getInstallableToml(): typemoq.IMock<TextDocument> { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType<TextDocument>(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[dependency-groups]\ndev = ["ruff", { include-group = "test" }]\ntest = ["pytest"]', + ); + return pyprojectToml; +} + +function getNonInstallableToml(): typemoq.IMock<TextDocument> { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType<TextDocument>(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); + return pyprojectToml; +} + +function getSomeFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +suite('PyProject.toml Create Env Features', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getNonInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file open in the editor on extension activate', async () => { + const someFile = getSomeFile(); + getOpenTextDocumentsStub.returns([someFile.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); + + handler(pyprojectToml.object); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Non Installable pyproject.toml is changed to Installable', async () => { + getOpenTextDocumentsStub.returns([]); + + let openHandler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + openHandler = callback; + return new FakeDisposable(); + }); + + let changeHandler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + changeHandler = callback; + return new FakeDisposable(); + }); + + const nonInatallablePyprojectToml = getNonInstallableToml(); + const installablePyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + openHandler(nonInatallablePyprojectToml.object); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + changeHandler(installablePyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Some random file is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.notCalled); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/cacheableLocatorService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/cacheableLocatorService.unit.test.ts deleted file mode 100644 index a6dc8b0b61bc..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/cacheableLocatorService.unit.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -// 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 { PythonInterpreter } 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<PythonInterpreter[]> { - return this.mockLocator.getInterpretersImplementation(); - } - protected getCachedInterpreters(_resource?: Uri): PythonInterpreter[] | undefined { - return this.mockLocator.getCachedInterpreters(); - } - protected async cacheInterpreters(_interpreters: PythonInterpreter[], _resource?: Uri) { - return this.mockLocator.cacheInterpreters(); - } - protected getCacheKey(_resource?: Uri) { - return this.mockLocator.getCacheKey(); - } - } - class MockLocator { - public async getInterpretersImplementation(): Promise<PythonInterpreter[]> { - return []; - } - public getCachedInterpreters(): PythonInterpreter[] | undefined { - return; - } - public async cacheInterpreters() { - return; - } - 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<PythonInterpreter[]> { - return []; - } - protected getCachedInterpreters(_resource?: Uri): PythonInterpreter[] | undefined { - return []; - } - protected async cacheInterpreters(_interpreters: PythonInterpreter[], _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 deleted file mode 100644 index b91521e1f74e..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/condaEnvFileService.unit.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -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 { InterpreterType } 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) => { - return 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) => { - return { - 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].type, InterpreterType.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 deleted file mode 100644 index 0b98d2e07136..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/condaEnvService.unit.test.ts +++ /dev/null @@ -1,498 +0,0 @@ -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 { InterpreterType } 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) => { - return 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].type, InterpreterType.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].type, InterpreterType.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) => { - return 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].type, InterpreterType.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].type, InterpreterType.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) => { - return 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].type, InterpreterType.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) => { - return 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].type, InterpreterType.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) => { - return 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].type, InterpreterType.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) => { - return 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].type, InterpreterType.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) => { - return 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].type, InterpreterType.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) => { - return 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 deleted file mode 100644 index b5c07aa315d5..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/condaHelper.unit.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -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 index d5b7e7938949..95e94cfc4584 100644 --- a/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts @@ -1,1010 +1,33 @@ -// 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 * as sinon from 'sinon'; 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - type: InterpreterType.Unknown, - version: new SemVer('0.0.0-alpha'), - sysPrefix: '', - sysVersion: '' -}; +import { CondaService } from '../../../../client/pythonEnvironments/common/environmentManagers/condaService'; +import { Conda } from '../../../../client/pythonEnvironments/common/environmentManagers/conda'; 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) + 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: PythonInterpreter[] = [ - { - displayName: 'One', - path: path.join(environmentsPath, 'path1', 'one.exe'), - companyDisplayName: 'One 1', - version: new SemVer('1.0.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Anaconda', - path: condaPythonExePath, - companyDisplayName: 'Two 2', - version: new SemVer('1.11.0'), - type: InterpreterType.Conda - }, - { - displayName: 'Three', - path: path.join(environmentsPath, 'path2', 'one.exe'), - companyDisplayName: 'Three 3', - version: new SemVer('2.10.1'), - type: InterpreterType.Unknown - }, - { - displayName: 'Seven', - path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), - companyDisplayName: 'Continuum Analytics, Inc.', - type: InterpreterType.Unknown - } - ].map((item) => { - return { ...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: PythonInterpreter[] = [ - { - displayName: 'One', - path: path.join(environmentsPath, 'path1', 'one.exe'), - companyDisplayName: 'One 1', - version: new SemVer('1.0.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 1', - version: new SemVer('1.11.0'), - type: InterpreterType.Conda - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 2.11', - version: new SemVer('2.11.0'), - type: InterpreterType.Conda - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 2.31', - version: new SemVer('2.31.0'), - type: InterpreterType.Conda - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 2.21', - version: new SemVer('2.21.0'), - type: InterpreterType.Conda - }, - { - displayName: 'Three', - path: path.join(environmentsPath, 'path2', 'one.exe'), - companyDisplayName: 'Three 3', - version: new SemVer('2.10.1'), - type: InterpreterType.Unknown - }, - { - displayName: 'Seven', - path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), - companyDisplayName: 'Continuum Analytics, Inc.', - type: InterpreterType.Unknown - } - ].map((item) => { - return { ...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: PythonInterpreter[] = [ - { - displayName: 'One', - path: path.join(environmentsPath, 'path1', 'one.exe'), - companyDisplayName: 'One 1', - version: new SemVer('1.0.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 1', - version: new SemVer('1.11.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 2.11', - version: new SemVer('2.11.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 2.31', - version: new SemVer('2.31.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Anaconda', - path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), - companyDisplayName: 'Two 2.21', - version: new SemVer('2.21.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Three', - path: path.join(environmentsPath, 'path2', 'one.exe'), - companyDisplayName: 'Three 3', - version: new SemVer('2.10.1'), - type: InterpreterType.Unknown - }, - { - displayName: 'Seven', - path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), - companyDisplayName: 'Continuum Analytics, Inc.', - type: InterpreterType.Unknown - } - ].map((item) => { - return { ...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: PythonInterpreter[] = [ - { - displayName: 'One', - path: path.join(environmentsPath, 'path1', 'one.exe'), - companyDisplayName: 'One 1', - version: new SemVer('1.0.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Anaconda', - path: condaPythonExePath, - companyDisplayName: 'Two 2', - version: new SemVer('1.11.0'), - type: InterpreterType.Unknown - }, - { - displayName: 'Three', - path: path.join(environmentsPath, 'path2', 'one.exe'), - companyDisplayName: 'Three 3', - version: new SemVer('2.10.1'), - type: InterpreterType.Unknown - }, - { - displayName: 'Seven', - path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), - companyDisplayName: 'Continuum Analytics, Inc.', - type: InterpreterType.Unknown - } - ].map((item) => { - return { ...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); + condaService = new CondaService(platformService.object, fileSystem.object); + sinon.stub(Conda, 'getConda').callsFake(() => Promise.resolve(undefined)); }); + teardown(() => sinon.restore()); type InterpreterSearchTestParams = { pythonPath: string; @@ -1018,26 +41,26 @@ suite('Interpreters Conda Service', () => { pythonPath: path.join('users', 'foo', 'envs', 'test1', 'python'), environmentName: 'test1', isLinux: true, - expectedCondaPath: path.join('users', 'foo', 'bin', 'conda') + 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') + 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') + 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') - } + expectedCondaPath: path.join('users', 'foo', 'conda.exe'), + }, ]; testsForInterpreter.forEach((t) => { @@ -1053,13 +76,13 @@ suite('Interpreters Conda Service', () => { return true; } return false; - }) - ) + }), + ), ) .returns(() => Promise.resolve(true)); const condaFile = await condaService.getCondaFileFromInterpreter(t.pythonPath, t.environmentName); - assert.equal(condaFile, t.expectedCondaPath); + assert.strictEqual(condaFile, t.expectedCondaPath); }); test(`Finds conda.exe for different ${t.environmentName}`, async () => { platformService.setup((p) => p.isLinux).returns(() => t.isLinux); @@ -1073,8 +96,8 @@ suite('Interpreters Conda Service', () => { return true; } return false; - }) - ) + }), + ), ) .returns(() => Promise.resolve(true)); @@ -1082,9 +105,9 @@ suite('Interpreters Conda Service', () => { // 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); + assert.strictEqual(condaFile, t.expectedCondaPath); } else { - assert.equal(condaFile, undefined); + assert.strictEqual(condaFile, 'conda'); } }); }); diff --git a/src/test/pythonEnvironments/discovery/locators/hasProviderFactory.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/hasProviderFactory.unit.test.ts deleted file mode 100644 index 465c0c039f32..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/hasProviderFactory.unit.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -// 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 { InterpreterHashProviderFactory } 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: InterpreterHashProviderFactory; - setup(() => { - configService = mock(ConfigurationService); - windowsStoreInterpreter = mock(WindowsStoreInterpreter); - standardHashProvider = mock(InterpreterHashProvider); - const windowsStoreInstance = instance(windowsStoreInterpreter); - (windowsStoreInstance as any).then = undefined; - factory = new InterpreterHashProviderFactory( - 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 deleted file mode 100644 index 51b6ea5ba463..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/hashProvider.unit.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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 deleted file mode 100644 index be005fda70e2..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/helpers.unit.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter[] = []; - ['conda', 'virtualenv', 'mac', 'pyenv'].forEach((name) => { - const interpreter = { - architecture: Architecture.Unknown, - displayName: name, - path: path.join('users', 'python', 'bin', name), - sysPrefix: name, - sysVersion: name, - type: InterpreterType.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: PythonInterpreter[] = []; - const expectedInterpreters: PythonInterpreter[] = []; - // 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, - type: InterpreterType.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, - type: InterpreterType.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, - type: InterpreterType.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: PythonInterpreter[] = []; - const expectedInterpreters: PythonInterpreter[] = []; - ['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 ? InterpreterType.Unknown : InterpreterType.Pipenv; - const interpreter = { - architecture: Architecture.Unknown, - displayName: name, - path: path.join('users', 'python', 'bin', 'python.exe'), - sysPrefix: name, - sysVersion: name, - 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 deleted file mode 100644 index 55893d823eb2..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter = { - architecture: Architecture.Unknown, - displayName: typeName, - path: typeName, - sysPrefix: typeName, - sysVersion: typeName, - type: InterpreterType.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: PythonInterpreter = { - architecture: Architecture.Unknown, - displayName: typeName, - path: typeName, - sysPrefix: typeName, - sysVersion: typeName, - type: InterpreterType.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: PythonInterpreter = { - architecture: Architecture.Unknown, - displayName: typeName, - path: typeName, - sysPrefix: typeName, - sysVersion: typeName, - type: InterpreterType.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 deleted file mode 100644 index efdb582c402b..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/interpreterFilter.unit.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from 'chai'; -import { isHiddenInterpreter } from '../../../../client/pythonEnvironments/discovery/locators/services/interpreterFilter'; -import { InterpreterType, PythonInterpreter } 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): PythonInterpreter { - return { - path: interpreterPath, - sysVersion: '', - sysPrefix: '', - architecture: 1, - companyDisplayName: '', - displayName: 'python', - type: InterpreterType.WindowsStore, - envName: '', - envPath: '', - cachedEntry: false - }; - } - - doNotHideThesePaths.forEach((interpreterPath) => { - test(`Interpreter path should NOT be hidden - ${interpreterPath}`, () => { - const interpreter: PythonInterpreter = 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: PythonInterpreter = 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 deleted file mode 100644 index 335d49231505..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/interpreterLocatorService.testvirtualenvs.ts +++ /dev/null @@ -1,93 +0,0 @@ -// 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 { InterpreterType, PythonInterpreter } 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: PythonInterpreter[]; - 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.type === InterpreterType.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.type === InterpreterType.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.type === InterpreterType.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.type === InterpreterType.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 deleted file mode 100644 index 729b90f8a201..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/interpreterWatcherBuilder.unit.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -// 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 { IInterpreterWatcherRegistry, 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<IInterpreterWatcherRegistry>( - IInterpreterWatcherRegistry, - WORKSPACE_VIRTUAL_ENV_SERVICE - ) - ).thenReturn((watcher as any) as IInterpreterWatcherRegistry); - - 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<IInterpreterWatcherRegistry>( - IInterpreterWatcherRegistry, - WORKSPACE_VIRTUAL_ENV_SERVICE - ) - ).thenReturn((watcher as any) as IInterpreterWatcherRegistry); - - 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 deleted file mode 100644 index be97ba9b1744..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/knownPathService.unit.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -// 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(() => { - return { 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(() => { - return { 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(() => { - return { 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 deleted file mode 100644 index c2082f819929..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/pipEnvService.unit.test.ts +++ /dev/null @@ -1,337 +0,0 @@ -// 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.executable; - - 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 deleted file mode 100644 index 957fba97b8b6..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/progressService.unit.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -// 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 { PythonInterpreter } 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<PythonInterpreter[]>) => any; - public onLocating( - listener: (e: Promise<PythonInterpreter[]>) => any, - _thisArgs?: any, - _disposables?: Disposable[] - ): Disposable { - this.locatingCallback = listener; - return { dispose: noop }; - } - public getInterpreters(_resource?: Uri): Promise<PythonInterpreter[]> { - 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<PythonInterpreter[]>(); - 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<PythonInterpreter[]>(); - 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<PythonInterpreter[]>(); - locator1.locatingCallback!.bind(progress)(locatingDeferred1.promise); - - const locatingDeferred2 = createDeferred<PythonInterpreter[]>(); - locator2.locatingCallback!.bind(progress)(locatingDeferred2.promise); - - const locatingDeferred3 = createDeferred<PythonInterpreter[]>(); - 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 deleted file mode 100644 index 3888260a8bda..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/venv.unit.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -// 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(() => { - return { 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(() => { - return { 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/windowsKnownPathsLocator.functional.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsKnownPathsLocator.functional.test.ts new file mode 100644 index 000000000000..ebebf2a8220e --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/windowsKnownPathsLocator.functional.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { getOSType, OSType } from '../../../../client/common/utils/platform'; +import { PythonEnvKind, PythonEnvSource } from '../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo, PythonLocatorQuery } from '../../../../client/pythonEnvironments/base/locator'; +import { WindowsPathEnvVarLocator } from '../../../../client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator'; +import { ensureFSTree } from '../../../utils/fs'; +import { assertBasicEnvsEqual } from '../../base/locators/envTestUtils'; +import { createBasicEnv, getEnvs } from '../../base/common'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; + +const IS_WINDOWS = getOSType() === OSType.Windows; + +suite('Python envs locator - WindowsPathEnvVarLocator', async () => { + let cleanUps: (() => void)[]; + + const ENV_VAR = 'Path'; + + const datadir = path.join(__dirname, '.data'); + const ROOT1 = path.join(datadir, 'root1'); + const ROOT2 = path.join(datadir, 'parent', 'root2'); + const ROOT3 = path.join(datadir, 'root3'); + const ROOT4 = path.join(datadir, 'root4'); + const ROOT5 = path.join(datadir, 'root5'); + const ROOT6 = path.join(datadir, 'root6'); + const DOES_NOT_EXIST = path.join(datadir, '.does-not-exist'); + const dataTree = ` + ./.data/ + root1/ + python2.exe # matches on Windows (not actually executable though) + <python.exe> + <python2.7.exe> + <python3.exe> + <python3.8.exe> + <python3.8> + <python3.8.1rc1.10213.exe> # should match but doesn't + #<python27.exe> + #<python38.exe> + <python.3.8.exe> # should match but doesn't + python.txt + <my-python.exe> # should match but doesn't + <spam.exe> + spam.txt + parent/ + root2/ + <python2.exe> + <python2> + root3/ # empty + root4/ # no executables + subdir/ + spam.txt + python2 + #python.exe # matches on Windows (not actually executable though) + root5/ # executables only in subdir + subdir/ + <python2.exe> + <python2> + python2 + #python2.exe # matches on Windows (not actually executable though) + root6/ # no matching executables + <spam.exe> + spam.txt + <py> + <py.exe> + `.trimEnd(); + + suiteSetup(async function () { + if (!IS_WINDOWS) { + if (!process.env.PVSC_TEST_FORCE) { + this.skip(); + } + } + await ensureFSTree(dataTree, __dirname); + }); + setup(async () => { + if (!IS_WINDOWS) { + // eslint-disable-next-line global-require + const platformAPI = require('../../../../../client/common/utils/platform'); + const stub = sinon.stub(platformAPI, 'getOSType'); + stub.returns(OSType.Windows); + } + sinon.stub(externalDependencies, 'inExperiment').returns(true); + cleanUps = []; + + const oldSearchPath = process.env[ENV_VAR]; + cleanUps.push(() => { + process.env[ENV_VAR] = oldSearchPath; + }); + }); + teardown(() => { + cleanUps.forEach((run) => { + try { + run(); + } catch (err) { + console.log(err); + } + }); + sinon.restore(); + }); + + function getActiveLocator(...roots: string[]): WindowsPathEnvVarLocator { + process.env[ENV_VAR] = roots.join(path.delimiter); + const locator = new WindowsPathEnvVarLocator(); + cleanUps.push(() => locator.dispose()); + return locator; + } + + suite('iterEnvs()', () => { + test('no executables found', async () => { + const expected: BasicEnvInfo[] = []; + const locator = getActiveLocator(ROOT3, ROOT4, DOES_NOT_EXIST, ROOT5); + const query: PythonLocatorQuery | undefined = undefined; + + const iterator = locator.iterEnvs(query); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('no executables match', async () => { + const expected: BasicEnvInfo[] = []; + const locator = getActiveLocator(ROOT6, DOES_NOT_EXIST); + const query: PythonLocatorQuery | undefined = undefined; + + const iterator = locator.iterEnvs(query); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('some executables match', async () => { + const expected: BasicEnvInfo[] = [ + createBasicEnv(PythonEnvKind.System, path.join(ROOT1, 'python.exe'), [PythonEnvSource.PathEnvVar]), + + // We will expect the following once we switch + // to a better filter than isStandardPythonBinary(). + + // // On Windows we do not assume 2.7 for "python.exe". + // getEnv('', '2.7', path.join(ROOT2, 'python2.exe')), + // // This file isn't executable (but on Windows we can't tell that): + // getEnv('', '2.7', path.join(ROOT1, 'python2.exe')), + // getEnv('', '', path.join(ROOT1, 'python.exe')), + // getEnv('', '2.7', path.join(ROOT1, 'python2.7.exe')), + // getEnv('', '3.8', path.join(ROOT1, 'python3.8.exe')), + // getEnv('', '3', path.join(ROOT1, 'python3.exe')), + ]; + const locator = getActiveLocator(ROOT2, ROOT6, ROOT1); + const query: PythonLocatorQuery | undefined = undefined; + + const iterator = locator.iterEnvs(query); + const envs = await getEnvs(iterator); + + assertBasicEnvsEqual(envs, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/windowsRegistryService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsRegistryService.unit.test.ts deleted file mode 100644 index f58e710a4eb0..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/windowsRegistryService.unit.test.ts +++ /dev/null @@ -1,992 +0,0 @@ -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 { InterpreterType } 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) => { - return 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].type, InterpreterType.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].type, InterpreterType.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 deleted file mode 100644 index ef39d265557b..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/windowsStoreInterpreter.unit.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -// 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/workspaceVirtualEnvService.test.ts b/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.test.ts deleted file mode 100644 index 7bb93205ed0c..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -// 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: 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 () { - 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 deleted file mode 100644 index 5baec513d88c..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.unit.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 5c4aab34ed58..000000000000 --- a/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvWatcherService.unit.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -// 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 deleted file mode 100644 index 0e25593ea913..000000000000 --- a/src/test/pythonEnvironments/discovery/subenv.unit.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -// 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 { InterpreterType } 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<InterpreterType | undefined>; - pyenv(python: string): Promise<InterpreterType | undefined>; - pipenv(python: string): Promise<InterpreterType | undefined>; - virtualenv(python: string): Promise<InterpreterType | 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(InterpreterType.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(InterpreterType.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(InterpreterType.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(InterpreterType.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/executable.unit.test.ts b/src/test/pythonEnvironments/info/executable.unit.test.ts index 56e81fca8ad5..bb6ecd7acabc 100644 --- a/src/test/pythonEnvironments/info/executable.unit.test.ts +++ b/src/test/pythonEnvironments/info/executable.unit.test.ts @@ -2,20 +2,13 @@ // 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 { IMock, Mock, MockBehavior, It } from 'typemoq'; +import { ExecutionResult, ShellOptions, 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>; + shellExec(command: string, options: ShellOptions | undefined): Promise<ExecutionResult<string>>; } suite('getExecutablePath()', () => { @@ -28,11 +21,10 @@ suite('getExecutablePath()', () => { 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)) + deps.setup((d) => d.shellExec(`${python.command} -c "import sys;print(sys.executable)"`, It.isAny())) // Return the expected value. .returns(() => Promise.resolve({ stdout: expected })); - const exec = async (c: string, a: string[]) => deps.object.exec(c, a); + const exec = async (c: string, a: ShellOptions | undefined) => deps.object.shellExec(c, a); const result = await getExecutablePath(python, exec); @@ -42,15 +34,14 @@ suite('getExecutablePath()', () => { 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)) + deps.setup((d) => d.shellExec(`${python.command} -c "import sys;print(sys.executable)"`, It.isAny())) // Throw an error. .returns(() => Promise.reject(new StdErrError(stderr))); - const exec = async (c: string, a: string[]) => deps.object.exec(c, a); + const exec = async (c: string, a: ShellOptions | undefined) => deps.object.shellExec(c, a); - const result = getExecutablePath(python, exec); + const promise = getExecutablePath(python, exec); - expect(result).to.eventually.be.rejectedWith(stderr); + expect(promise).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 index 992257c8f208..967454dd6c7e 100644 --- a/src/test/pythonEnvironments/info/interpreter.unit.test.ts +++ b/src/test/pythonEnvironments/info/interpreter.unit.test.ts @@ -4,15 +4,14 @@ 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 { IMock, It, It as TypeMoqIt, Mock, MockBehavior } from 'typemoq'; +import { ShellOptions, 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'); +const script = pathJoin(EXTENSION_ROOT_DIR_FOR_TESTS, 'python_files', 'interpreterInfo.py'); suite('extractInterpreterInfo()', () => { // Tests go here. @@ -23,7 +22,7 @@ type ShellExecResult = { stderr?: string; }; interface IDeps { - shellExec(command: string, timeout: number): Promise<ShellExecResult>; + shellExec(command: string, options?: ShellOptions | undefined): Promise<ShellExecResult>; } suite('getInterpreterInfo()', () => { @@ -39,14 +38,18 @@ suite('getInterpreterInfo()', () => { 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 + is64Bit: true, }; - const cmd = `"${python.command}" "${isolated}" "${script}"`; + const cmd = `"${python.command}" "${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); + .setup((d) => d.shellExec(cmd, It.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); await getInterpreterInfo(python, shellExec); @@ -58,15 +61,19 @@ suite('getInterpreterInfo()', () => { 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 + is64Bit: true, }; const _python = buildPythonExecInfo(' path to /my python '); - const cmd = `" path to /my python " "${isolated}" "${script}"`; + const cmd = `" path to /my python " "${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); + .setup((d) => d.shellExec(cmd, It.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); await getInterpreterInfo(_python, shellExec); @@ -78,15 +85,19 @@ suite('getInterpreterInfo()', () => { 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 + 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}"`; + const cmd = `"path/to/conda" "run" "-n" "my-env" "python" "${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); + .setup((d) => d.shellExec(cmd, It.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); await getInterpreterInfo(_python, shellExec); @@ -97,21 +108,25 @@ suite('getInterpreterInfo()', () => { const expected = { architecture: Architecture.x64, path: python.command, - version: new SemVer('3.7.5-candidate'), + version: new SemVer('3.7.5-candidate1'), sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', - sysVersion: undefined + 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 + 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); + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); const result = await getInterpreterInfo(python, shellExec); @@ -125,19 +140,23 @@ suite('getInterpreterInfo()', () => { path: python.command, version: new SemVer('3.7.5'), sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', - sysVersion: undefined + 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 + 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); + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); const result = await getInterpreterInfo(python, shellExec); @@ -151,19 +170,23 @@ suite('getInterpreterInfo()', () => { path: python.command, version: new SemVer('3.7.5-candidate'), sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', - sysVersion: undefined + 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 + 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); + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); const result = await getInterpreterInfo(python, shellExec); @@ -177,7 +200,7 @@ suite('getInterpreterInfo()', () => { // 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 shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); const result = getInterpreterInfo(python, shellExec); @@ -191,7 +214,7 @@ suite('getInterpreterInfo()', () => { // 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 shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); const result = getInterpreterInfo(python, shellExec); @@ -204,7 +227,7 @@ suite('getInterpreterInfo()', () => { // 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 shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); const result = getInterpreterInfo(python, shellExec); diff --git a/src/test/pythonEnvironments/info/pythonVersion.unit.test.ts b/src/test/pythonEnvironments/info/pythonVersion.unit.test.ts deleted file mode 100644 index 70dbd90cacfa..000000000000 --- a/src/test/pythonEnvironments/info/pythonVersion.unit.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -// 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.ts b/src/test/pythonEnvironments/legacyIOC.ts new file mode 100644 index 000000000000..c521569c77d8 --- /dev/null +++ b/src/test/pythonEnvironments/legacyIOC.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { instance, mock } from 'ts-mockito'; +import { IServiceContainer, IServiceManager } from '../../client/ioc/types'; +import { IDiscoveryAPI } from '../../client/pythonEnvironments/base/locator'; +import { initializeExternalDependencies } from '../../client/pythonEnvironments/common/externalDependencies'; +import { registerNewDiscoveryForIOC } from '../../client/pythonEnvironments/legacyIOC'; + +/** + * This is here to support old tests. + * @deprecated + */ +export async function registerForIOC( + serviceManager: IServiceManager, + serviceContainer: IServiceContainer, +): Promise<void> { + initializeExternalDependencies(serviceContainer); + // The old tests do not need real instances, directly pass in mocks. + registerNewDiscoveryForIOC(serviceManager, instance(mock<IDiscoveryAPI>())); +} diff --git a/src/test/pythonEnvironments/nativeAPI.unit.test.ts b/src/test/pythonEnvironments/nativeAPI.unit.test.ts new file mode 100644 index 000000000000..a3696b59c6ac --- /dev/null +++ b/src/test/pythonEnvironments/nativeAPI.unit.test.ts @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ + +import { assert } from 'chai'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as nativeAPI from '../../client/pythonEnvironments/nativeAPI'; +import { IDiscoveryAPI } from '../../client/pythonEnvironments/base/locator'; +import { + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonFinder, +} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; +import { Architecture, getPathEnvVariable, isWindows } from '../../client/common/utils/platform'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from '../../client/pythonEnvironments/base/info'; +import { NativePythonEnvironmentKind } from '../../client/pythonEnvironments/base/locators/common/nativePythonUtils'; +import * as condaApi from '../../client/pythonEnvironments/common/environmentManagers/conda'; +import * as pyenvApi from '../../client/pythonEnvironments/common/environmentManagers/pyenv'; +import * as pw from '../../client/pythonEnvironments/base/locators/common/pythonWatcher'; +import * as ws from '../../client/common/vscodeApis/workspaceApis'; + +suite('Native Python API', () => { + let api: IDiscoveryAPI; + let mockFinder: typemoq.IMock<NativePythonFinder>; + let setCondaBinaryStub: sinon.SinonStub; + let getCondaPathSettingStub: sinon.SinonStub; + let getCondaEnvDirsStub: sinon.SinonStub; + let setPyEnvBinaryStub: sinon.SinonStub; + let createPythonWatcherStub: sinon.SinonStub; + let mockWatcher: typemoq.IMock<pw.PythonWatcher>; + let getWorkspaceFoldersStub: sinon.SinonStub; + + const basicEnv: NativeEnvInfo = { + displayName: 'Basic Python', + name: 'basic_python', + executable: '/usr/bin/python', + kind: NativePythonEnvironmentKind.LinuxGlobal, + version: `3.12.0`, + prefix: '/usr/bin', + }; + + const basicEnv2: NativeEnvInfo = { + displayName: 'Basic Python', + name: 'basic_python', + executable: '/usr/bin/python', + kind: NativePythonEnvironmentKind.LinuxGlobal, + version: undefined, // this is intentionally set to trigger resolve + prefix: '/usr/bin', + }; + + const expectedBasicEnv: PythonEnvInfo = { + arch: Architecture.Unknown, + id: '/usr/bin/python', + detailedDisplayName: 'Python 3.12.0 (basic_python)', + display: 'Python 3.12.0 (basic_python)', + distro: { org: '' }, + executable: { filename: '/usr/bin/python', sysPrefix: '/usr/bin', ctime: -1, mtime: -1 }, + kind: PythonEnvKind.System, + location: '/usr/bin/python', + source: [], + name: 'basic_python', + type: undefined, + version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 }, + }; + + const conda: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: '/home/user/.conda/envs/conda_python/python', + kind: NativePythonEnvironmentKind.Conda, + version: `3.12.0`, + prefix: '/home/user/.conda/envs/conda_python', + }; + + const conda1: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: '/home/user/.conda/envs/conda_python/python', + kind: NativePythonEnvironmentKind.Conda, + version: undefined, // this is intentionally set to test conda without python + prefix: '/home/user/.conda/envs/conda_python', + }; + + const conda2: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: undefined, // this is intentionally set to test env with no executable + kind: NativePythonEnvironmentKind.Conda, + version: undefined, // this is intentionally set to test conda without python + prefix: '/home/user/.conda/envs/conda_python', + }; + + const exePath = isWindows() + ? path.join('/home/user/.conda/envs/conda_python', 'python.exe') + : path.join('/home/user/.conda/envs/conda_python', 'python'); + + const expectedConda1: PythonEnvInfo = { + arch: Architecture.Unknown, + detailedDisplayName: 'Python 3.12.0 (conda_python)', + display: 'Python 3.12.0 (conda_python)', + distro: { org: '' }, + id: '/home/user/.conda/envs/conda_python/python', + executable: { + filename: '/home/user/.conda/envs/conda_python/python', + sysPrefix: '/home/user/.conda/envs/conda_python', + ctime: -1, + mtime: -1, + }, + kind: PythonEnvKind.Conda, + location: '/home/user/.conda/envs/conda_python', + source: [], + name: 'conda_python', + type: PythonEnvType.Conda, + version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 }, + }; + + const expectedConda2: PythonEnvInfo = { + arch: Architecture.Unknown, + detailedDisplayName: 'Conda Python', + display: 'Conda Python', + distro: { org: '' }, + id: exePath, + executable: { + filename: exePath, + sysPrefix: '/home/user/.conda/envs/conda_python', + ctime: -1, + mtime: -1, + }, + kind: PythonEnvKind.Conda, + location: '/home/user/.conda/envs/conda_python', + source: [], + name: 'conda_python', + type: PythonEnvType.Conda, + version: { sysVersion: undefined, major: -1, minor: -1, micro: -1 }, + }; + + setup(() => { + setCondaBinaryStub = sinon.stub(condaApi, 'setCondaBinary'); + getCondaEnvDirsStub = sinon.stub(condaApi, 'getCondaEnvDirs'); + getCondaPathSettingStub = sinon.stub(condaApi, 'getCondaPathSetting'); + setPyEnvBinaryStub = sinon.stub(pyenvApi, 'setPyEnvBinary'); + getWorkspaceFoldersStub = sinon.stub(ws, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([]); + + createPythonWatcherStub = sinon.stub(pw, 'createPythonWatcher'); + mockWatcher = typemoq.Mock.ofType<pw.PythonWatcher>(); + createPythonWatcherStub.returns(mockWatcher.object); + + mockWatcher.setup((w) => w.watchWorkspace(typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.watchPath(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.unwatchWorkspace(typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.unwatchPath(typemoq.It.isAny())).returns(() => undefined); + + mockFinder = typemoq.Mock.ofType<NativePythonFinder>(); + api = nativeAPI.createNativeEnvironmentsApi(mockFinder.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Trigger refresh without resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Trigger refresh with resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv2]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(basicEnv)) + .verifiable(typemoq.Times.once()); + + api.triggerRefresh(); + await api.getRefreshPromise(); + + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Trigger refresh and use refresh promise API', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + api.triggerRefresh(); + await api.getRefreshPromise(); + + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Conda environment with resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda1]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(conda)) + .verifiable(typemoq.Times.once()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda1]); + }); + + test('Ensure no duplication on resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda1]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(conda)) + .verifiable(typemoq.Times.once()); + + await api.triggerRefresh(); + await api.resolveEnv('/home/user/.conda/envs/conda_python/python'); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda1]); + }); + + test('Conda environment with no python', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda2]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda2]); + }); + + test('Refresh promise undefined after refresh', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + assert.isUndefined(api.getRefreshPromise()); + }); + + test('Setting conda binary', async () => { + getCondaPathSettingStub.returns(undefined); + getCondaEnvDirsStub.resolves(undefined); + const condaFakeDir = getPathEnvVariable()[0]; + const condaMgr: NativeEnvManagerInfo = { + tool: 'Conda', + executable: path.join(condaFakeDir, 'conda'), + }; + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [condaMgr]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + await api.triggerRefresh(); + assert.isTrue(setCondaBinaryStub.calledOnceWith(condaMgr.executable)); + }); + + test('Setting pyenv binary', async () => { + const pyenvMgr: NativeEnvManagerInfo = { + tool: 'PyEnv', + executable: '/usr/bin/pyenv', + }; + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [pyenvMgr]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + await api.triggerRefresh(); + assert.isTrue(setPyEnvBinaryStub.calledOnceWith(pyenvMgr.executable)); + }); +}); diff --git a/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts new file mode 100644 index 000000000000..b6182da8111f --- /dev/null +++ b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { + getNativePythonFinder, + isNativeEnvInfo, + NativeEnvInfo, + NativePythonFinder, +} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; +import * as windowsApis from '../../client/common/vscodeApis/windowApis'; +import { MockOutputChannel } from '../mockClasses'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Native Python Finder', () => { + let finder: NativePythonFinder; + let createLogOutputChannelStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock<WorkspaceConfiguration>; + let getWorkspaceFolderPathsStub: sinon.SinonStub; + + setup(() => { + createLogOutputChannelStub = sinon.stub(windowsApis, 'createLogOutputChannel'); + createLogOutputChannelStub.returns(new MockOutputChannel('locator')); + + getWorkspaceFolderPathsStub = sinon.stub(workspaceApis, 'getWorkspaceFolderPaths'); + getWorkspaceFolderPathsStub.returns([]); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + configMock = typemoq.Mock.ofType<WorkspaceConfiguration>(); + configMock.setup((c) => c.get<string>('venvPath')).returns(() => undefined); + configMock.setup((c) => c.get<string[]>('venvFolders')).returns(() => []); + configMock.setup((c) => c.get<string>('condaPath')).returns(() => ''); + configMock.setup((c) => c.get<string>('poetryPath')).returns(() => ''); + getConfigurationStub.returns(configMock.object); + + finder = getNativePythonFinder(); + }); + + teardown(() => { + sinon.restore(); + }); + + suiteTeardown(() => { + finder.dispose(); + }); + + test('Refresh should return python environments', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + }); + + test('Resolve should return python environments with version', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + + // pick and env without version + const env: NativeEnvInfo | undefined = envs + .filter((e) => isNativeEnvInfo(e)) + .find((e) => e.version && e.version.length > 0 && (e.executable || (e as NativeEnvInfo).prefix)); + + if (env) { + env.version = undefined; + } else { + assert.fail('Expected at least one env with valid version'); + } + + const envPath = env.executable ?? env.prefix; + if (envPath) { + const resolved = await finder.resolve(envPath); + assert.isString(resolved.version, 'Version must be a string'); + assert.isTrue((resolved?.version?.length ?? 0) > 0, 'Version must not be empty'); + } else { + assert.fail('Expected either executable or prefix to be defined'); + } + }); +}); diff --git a/src/test/pythonFiles/autocomp/deco.py b/src/test/pythonFiles/autocomp/deco.py deleted file mode 100644 index b843741ef647..000000000000 --- a/src/test/pythonFiles/autocomp/deco.py +++ /dev/null @@ -1,6 +0,0 @@ - -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 deleted file mode 100644 index a0d62874538f..000000000000 --- a/src/test/pythonFiles/autocomp/doc.py +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 507c5fed967c..000000000000 --- a/src/test/pythonFiles/autocomp/five.py +++ /dev/null @@ -1,2 +0,0 @@ -import four -four.showMessage() diff --git a/src/test/pythonFiles/autocomp/four.py b/src/test/pythonFiles/autocomp/four.py deleted file mode 100644 index 470338f71157..000000000000 --- a/src/test/pythonFiles/autocomp/four.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- 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 deleted file mode 100644 index 0ff88d80dffc..000000000000 --- a/src/test/pythonFiles/autocomp/hoverTest.py +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 0d0c98ed1cde..000000000000 --- a/src/test/pythonFiles/autocomp/imp.py +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 05b92f5cd581..000000000000 --- a/src/test/pythonFiles/autocomp/lamb.py +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 3d4a54cbc145..000000000000 --- a/src/test/pythonFiles/autocomp/misc.py +++ /dev/null @@ -1,1905 +0,0 @@ -"""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 deleted file mode 100644 index 5e5708fd92f0..000000000000 --- a/src/test/pythonFiles/autocomp/one.py +++ /dev/null @@ -1,31 +0,0 @@ - -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 deleted file mode 100644 index 79edec69ae1a..000000000000 --- a/src/test/pythonFiles/autocomp/pep484.py +++ /dev/null @@ -1,12 +0,0 @@ - -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 deleted file mode 100644 index d8cd0300ed0d..000000000000 --- a/src/test/pythonFiles/autocomp/pep526.py +++ /dev/null @@ -1,22 +0,0 @@ - - -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 deleted file mode 100644 index 9f74959ef14b..000000000000 --- a/src/test/pythonFiles/autocomp/suppress.py +++ /dev/null @@ -1,6 +0,0 @@ -"string" #comment -""" -content -""" -#comment -'un#closed diff --git a/src/test/pythonFiles/autocomp/three.py b/src/test/pythonFiles/autocomp/three.py deleted file mode 100644 index 35ad7f399172..000000000000 --- a/src/test/pythonFiles/autocomp/three.py +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 99a6e3c4bdf1..000000000000 --- a/src/test/pythonFiles/autocomp/two.py +++ /dev/null @@ -1,6 +0,0 @@ -class ct: - def fun(): - """ - This is fun - """ - pass \ No newline at end of file diff --git a/src/test/pythonFiles/definition/await.test.py b/src/test/pythonFiles/definition/await.test.py deleted file mode 100644 index 7b4acd876c27..000000000000 --- a/src/test/pythonFiles/definition/await.test.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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 deleted file mode 100644 index 507c5fed967c..000000000000 --- a/src/test/pythonFiles/definition/five.py +++ /dev/null @@ -1,2 +0,0 @@ -import four -four.showMessage() diff --git a/src/test/pythonFiles/definition/four.py b/src/test/pythonFiles/definition/four.py deleted file mode 100644 index 470338f71157..000000000000 --- a/src/test/pythonFiles/definition/four.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- 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/definitions.py b/src/test/pythonFiles/definition/navigation/definitions.py deleted file mode 100644 index a8379a49f960..000000000000 --- a/src/test/pythonFiles/definition/navigation/definitions.py +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index deb6d78edc15..000000000000 --- a/src/test/pythonFiles/definition/navigation/usages.py +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index f1e3d75ffcbc..000000000000 --- a/src/test/pythonFiles/definition/one.py +++ /dev/null @@ -1,46 +0,0 @@ - -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 deleted file mode 100644 index 35ad7f399172..000000000000 --- a/src/test/pythonFiles/definition/three.py +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 99a6e3c4bdf1..000000000000 --- a/src/test/pythonFiles/definition/two.py +++ /dev/null @@ -1,6 +0,0 @@ -class ct: - def fun(): - """ - This is fun - """ - pass \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/Lib/fileLib.py b/src/test/pythonFiles/exclusions/Lib/fileLib.py deleted file mode 100644 index 50000adeda40..000000000000 --- a/src/test/pythonFiles/exclusions/Lib/fileLib.py +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index dad1af98c7f5..000000000000 --- a/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index fe453b3fcc6a..000000000000 --- a/src/test/pythonFiles/exclusions/dir1/dir1file.py +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index fe453b3fcc6a..000000000000 --- a/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py +++ /dev/null @@ -1 +0,0 @@ - for \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/one.py b/src/test/pythonFiles/exclusions/one.py deleted file mode 100644 index 8c68a1c1fee2..000000000000 --- a/src/test/pythonFiles/exclusions/one.py +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index 9c331d6c49e1..000000000000 --- a/src/test/pythonFiles/folding/attach_server.py +++ /dev/null @@ -1,337 +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 <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/miscSamples.py b/src/test/pythonFiles/folding/miscSamples.py deleted file mode 100644 index 01495fb0ee9c..000000000000 --- a/src/test/pythonFiles/folding/miscSamples.py +++ /dev/null @@ -1,40 +0,0 @@ - -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 deleted file mode 100644 index 4f0f7c5ec235..000000000000 --- a/src/test/pythonFiles/folding/noComments.py +++ /dev/null @@ -1,285 +0,0 @@ -__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 deleted file mode 100644 index f5750dbfde78..000000000000 --- a/src/test/pythonFiles/folding/noDocStrings.py +++ /dev/null @@ -1,273 +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 <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/src/test/pythonFiles/folding/visualstudio_ipython_repl.py b/src/test/pythonFiles/folding/visualstudio_ipython_repl.py deleted file mode 100644 index 33aa109de971..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_ipython_repl.py +++ /dev/null @@ -1,430 +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. - -"""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 [] \ 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 deleted file mode 100644 index 473046639147..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py +++ /dev/null @@ -1,430 +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. - -"""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 deleted file mode 100644 index ec18ff8c63b0..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_py_debugger.py +++ /dev/null @@ -1,644 +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. -# 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 deleted file mode 100644 index 595922f8f9cc..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_py_repl.py +++ /dev/null @@ -1,520 +0,0 @@ -# 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 deleted file mode 100644 index e63158d6d4fd..000000000000 --- a/src/test/pythonFiles/formatting/autoPep8Formatted.py +++ /dev/null @@ -1,32 +0,0 @@ - -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 deleted file mode 100644 index 80cb3a445811..000000000000 --- a/src/test/pythonFiles/formatting/autopep8.output +++ /dev/null @@ -1,50 +0,0 @@ ---- 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 deleted file mode 100644 index 4c14d61f2b9b..000000000000 --- a/src/test/pythonFiles/formatting/black.output +++ /dev/null @@ -1,59 +0,0 @@ ---- 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 deleted file mode 100644 index e7bca8b1298c..000000000000 --- a/src/test/pythonFiles/formatting/blackFormatted.py +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index cbab6669e3b8..000000000000 --- a/src/test/pythonFiles/formatting/dummy.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 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 deleted file mode 100644 index 5b544bd8504d..000000000000 --- a/src/test/pythonFiles/formatting/fileToFormat.py +++ /dev/null @@ -1,22 +0,0 @@ - -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 deleted file mode 100644 index 8adfd1fa1233..000000000000 --- a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 3fe1b80fde86..000000000000 --- a/src/test/pythonFiles/formatting/formatWhenDirty.py +++ /dev/null @@ -1,3 +0,0 @@ -x = 0 -if x > 0: - x = 1 diff --git a/src/test/pythonFiles/formatting/formatWhenDirtyResult.py b/src/test/pythonFiles/formatting/formatWhenDirtyResult.py deleted file mode 100644 index d0ae06a2a59b..000000000000 --- a/src/test/pythonFiles/formatting/formatWhenDirtyResult.py +++ /dev/null @@ -1,3 +0,0 @@ -x = 0 -if x > 0: - x = 1 diff --git a/src/test/pythonFiles/formatting/pythonGrammar.py b/src/test/pythonFiles/formatting/pythonGrammar.py deleted file mode 100644 index 937cba401d3f..000000000000 --- a/src/test/pythonFiles/formatting/pythonGrammar.py +++ /dev/null @@ -1,1572 +0,0 @@ -# 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 deleted file mode 100644 index 43897b1753b0..000000000000 --- a/src/test/pythonFiles/formatting/yapf.output +++ /dev/null @@ -1,57 +0,0 @@ ---- 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 deleted file mode 100644 index aa3b079379a2..000000000000 --- a/src/test/pythonFiles/formatting/yapfFormatted.py +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index a0f765a5a41f..000000000000 --- a/src/test/pythonFiles/hover/functionHover.py +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index b54311aa83c1..000000000000 --- a/src/test/pythonFiles/hover/stringFormat.py +++ /dev/null @@ -1,7 +0,0 @@ - -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 deleted file mode 100644 index 7b625a769243..000000000000 --- a/src/test/pythonFiles/linting/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""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 deleted file mode 100644 index 99ff2b9f819c..000000000000 --- a/src/test/pythonFiles/linting/flake8config/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[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 deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/flake8config/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""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 deleted file mode 100644 index d93fa56f7e8a..000000000000 --- a/src/test/pythonFiles/linting/minCheck.py +++ /dev/null @@ -1 +0,0 @@ -filter(lambda x: x == 1, [1, 1, 2]) diff --git a/src/test/pythonFiles/linting/print.py b/src/test/pythonFiles/linting/print.py deleted file mode 100644 index fca61311fc84..000000000000 --- a/src/test/pythonFiles/linting/print.py +++ /dev/null @@ -1 +0,0 @@ -print x \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle b/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle deleted file mode 100644 index b7c78f49db84..000000000000 --- a/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle +++ /dev/null @@ -1,2 +0,0 @@ -[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 deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/pycodestyleconfig/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""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 deleted file mode 100644 index 19020834ad32..000000000000 --- a/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle +++ /dev/null @@ -1,2 +0,0 @@ -[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 deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/pydocstyleconfig27/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""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 deleted file mode 100644 index 59444d78c3a3..000000000000 --- a/src/test/pythonFiles/linting/pylintconfig/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[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 deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/pylintconfig/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""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 deleted file mode 100644 index f375c984aa2e..000000000000 --- a/src/test/pythonFiles/linting/pylintconfig/file2.py +++ /dev/null @@ -1,19 +0,0 @@ -"""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 deleted file mode 100644 index e8b578d93f11..000000000000 --- a/src/test/pythonFiles/linting/threeLineLints.py +++ /dev/null @@ -1,24 +0,0 @@ -"""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 deleted file mode 100644 index fff22dece1e5..000000000000 --- a/src/test/pythonFiles/markdown/aifc.md +++ /dev/null @@ -1,142 +0,0 @@ -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 deleted file mode 100644 index a4cc346d5531..000000000000 --- a/src/test/pythonFiles/markdown/aifc.pydoc +++ /dev/null @@ -1,134 +0,0 @@ -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 deleted file mode 100644 index e5914dcbadde..000000000000 --- a/src/test/pythonFiles/markdown/anydbm.md +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 2d46b5881789..000000000000 --- a/src/test/pythonFiles/markdown/anydbm.pydoc +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index b5ece21c1faf..000000000000 --- a/src/test/pythonFiles/markdown/astroid.md +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 84d58487ead5..000000000000 --- a/src/test/pythonFiles/markdown/astroid.pydoc +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index d28c1e290abe..000000000000 --- a/src/test/pythonFiles/markdown/scipy.md +++ /dev/null @@ -1,47 +0,0 @@ -### 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 deleted file mode 100644 index 293445fbea5b..000000000000 --- a/src/test/pythonFiles/markdown/scipy.pydoc +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 276acddef787..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.distance.md +++ /dev/null @@ -1,54 +0,0 @@ -### 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 deleted file mode 100644 index cfc9b7008b99..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.distance.pydoc +++ /dev/null @@ -1,71 +0,0 @@ - -===================================================== -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 deleted file mode 100644 index 2d5e891db625..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.md +++ /dev/null @@ -1,65 +0,0 @@ -### 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 deleted file mode 100644 index 1613b94384b7..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.pydoc +++ /dev/null @@ -1,86 +0,0 @@ -============================================================= -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 deleted file mode 100644 index 01ed75727900..000000000000 --- a/src/test/pythonFiles/refactoring/source folder/with empty line.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index a449eb106f5c..000000000000 --- a/src/test/pythonFiles/refactoring/source folder/without empty line.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index ee941dd45ebf..000000000000 --- a/src/test/pythonFiles/refactoring/standAlone/refactor.py +++ /dev/null @@ -1,245 +0,0 @@ -# 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/signature/basicSig.py b/src/test/pythonFiles/signature/basicSig.py deleted file mode 100644 index 66ad4cbd0483..000000000000 --- a/src/test/pythonFiles/signature/basicSig.py +++ /dev/null @@ -1,2 +0,0 @@ -range(c, 1, - diff --git a/src/test/pythonFiles/signature/classCtor.py b/src/test/pythonFiles/signature/classCtor.py deleted file mode 100644 index baa4045489e7..000000000000 --- a/src/test/pythonFiles/signature/classCtor.py +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index c34faa6d231a..000000000000 --- a/src/test/pythonFiles/signature/ellipsis.py +++ /dev/null @@ -1 +0,0 @@ -print(a, b, c) diff --git a/src/test/pythonFiles/signature/noSigPy3.py b/src/test/pythonFiles/signature/noSigPy3.py deleted file mode 100644 index 3d814698b7fe..000000000000 --- a/src/test/pythonFiles/signature/noSigPy3.py +++ /dev/null @@ -1 +0,0 @@ -pow() diff --git a/src/test/pythonFiles/sorting/noconfig/after.py b/src/test/pythonFiles/sorting/noconfig/after.py deleted file mode 100644 index b768c396014c..000000000000 --- a/src/test/pythonFiles/sorting/noconfig/after.py +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index fcd7318b5c02..000000000000 --- a/src/test/pythonFiles/sorting/noconfig/before.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index fcd7318b5c02..000000000000 --- a/src/test/pythonFiles/sorting/noconfig/original.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 68da732e2b4b..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/.isort.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[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 deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/after.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/before.1.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/before.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/original.1.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index e1fd315dbf92..000000000000 --- a/src/test/pythonFiles/sorting/withconfig/original.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 31d6fc7b4a18..000000000000 --- a/src/test/pythonFiles/symbolFiles/childFile.py +++ /dev/null @@ -1,13 +0,0 @@ -"""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 deleted file mode 100644 index 27509dd2fcd6..000000000000 --- a/src/test/pythonFiles/symbolFiles/file.py +++ /dev/null @@ -1,87 +0,0 @@ -"""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 deleted file mode 100644 index 61aa87c55fed..000000000000 --- a/src/test/pythonFiles/symbolFiles/workspace2File.py +++ /dev/null @@ -1,13 +0,0 @@ -"""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/testFiles/counter/tests/__init__.py b/src/test/pythonFiles/testFiles/counter/tests/__init__.py deleted file mode 100644 index e02abfc9b0e1..000000000000 --- a/src/test/pythonFiles/testFiles/counter/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index 687af033be05..000000000000 --- a/src/test/pythonFiles/testFiles/counter/tests/test_unit_test_counter.py +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 33fb0fce9ba6..000000000000 --- a/src/test/pythonFiles/testFiles/cwd/src/tests/test_cwd.py +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index db18d3885488..000000000000 --- a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 4e1a6151deb1..000000000000 --- a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 4e1a6151deb1..000000000000 --- a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index b70c80df1619..000000000000 --- a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt +++ /dev/null @@ -1,14 +0,0 @@ -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/test_three.py b/src/test/pythonFiles/testFiles/multi/tests/more_tests/test_three.py deleted file mode 100644 index 9cea70ae7ca6..000000000000 --- a/src/test/pythonFiles/testFiles/multi/tests/more_tests/test_three.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/multi/tests/test_one.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index f3fef9c9b1eb..000000000000 --- a/src/test/pythonFiles/testFiles/multi/tests/test_two.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 8b0d557303f7..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/five.output +++ /dev/null @@ -1,121 +0,0 @@ -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 deleted file mode 100644 index 511f0b1c863c..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/four.output +++ /dev/null @@ -1,205 +0,0 @@ -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 deleted file mode 100644 index cafdaf5b906a..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/one.output +++ /dev/null @@ -1,211 +0,0 @@ -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 deleted file mode 100644 index 640132ffe72e..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.five.output +++ /dev/null @@ -1,567 +0,0 @@ -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 deleted file mode 100644 index 97c7e0e0216f..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.five.result +++ /dev/null @@ -1,11 +0,0 @@ -<?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 deleted file mode 100644 index aa01067d7925..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.four.output +++ /dev/null @@ -1,565 +0,0 @@ -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 deleted file mode 100644 index 828e4a74b06a..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.four.result +++ /dev/null @@ -1,12 +0,0 @@ -<?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 deleted file mode 100644 index 475ac92d3bb4..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.one.output +++ /dev/null @@ -1,558 +0,0 @@ -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 deleted file mode 100644 index 59de2cfcdcc8..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.one.result +++ /dev/null @@ -1,83 +0,0 @@ -<?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 deleted file mode 100644 index da1ec6bc25c9..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.three.output +++ /dev/null @@ -1,563 +0,0 @@ -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 deleted file mode 100644 index 828e4a74b06a..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.three.result +++ /dev/null @@ -1,12 +0,0 @@ -<?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 deleted file mode 100644 index b60e8229c55d..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result +++ /dev/null @@ -1,81 +0,0 @@ -<?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 deleted file mode 100644 index 31a5a5e9c34b..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.two.output +++ /dev/null @@ -1,560 +0,0 @@ -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 deleted file mode 100644 index 59de2cfcdcc8..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.two.result +++ /dev/null @@ -1,83 +0,0 @@ -<?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 deleted file mode 100644 index 4825f3a4db3b..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index c9a76c07f933..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/test_root.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 734b84cd342e..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index ad89d873e879..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 507e6af02063..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index a57dae74d180..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/three.output +++ /dev/null @@ -1,555 +0,0 @@ -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 deleted file mode 100644 index 25fcf10c93d5..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/two.output +++ /dev/null @@ -1,211 +0,0 @@ -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 deleted file mode 100644 index c7b9d058f784..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/five.output +++ /dev/null @@ -1,375 +0,0 @@ -[ - { - "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 deleted file mode 100644 index 87d7abeb58ce..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/five.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?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 deleted file mode 100644 index c7b9d058f784..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/four.output +++ /dev/null @@ -1,375 +0,0 @@ -[ - { - "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 deleted file mode 100644 index b13d0a4c1fc3..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/four.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?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 deleted file mode 100644 index c7b9d058f784..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/one.output +++ /dev/null @@ -1,375 +0,0 @@ -[ - { - "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 deleted file mode 100644 index e4d7a513e119..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/one.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?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 deleted file mode 100644 index c7b9d058f784..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/three.output +++ /dev/null @@ -1,375 +0,0 @@ -[ - { - "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 deleted file mode 100644 index 0d1e912f656c..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/three.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?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 deleted file mode 100644 index af1ee36ca7b7..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/two.again.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?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 deleted file mode 100644 index c7b9d058f784..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/two.output +++ /dev/null @@ -1,375 +0,0 @@ -[ - { - "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 deleted file mode 100644 index e4d7a513e119..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/two.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?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 deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/single/test_root.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/single/tests/test_one.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 72db843aa2af..000000000000 --- a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index abac1b49023f..000000000000 --- a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/standard/test_root.py +++ /dev/null @@ -1,19 +0,0 @@ -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/external.py b/src/test/pythonFiles/testFiles/standard/tests/external.py deleted file mode 100644 index e7446cadb184..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/external.py +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 129bc168f0d5..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_another_pytest.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100644 index 60df159b4c6d..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_foreign_nested_tests.py +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index dc5798306bb6..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_pytest.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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 deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index ad89d873e879..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_two.py +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 507e6af02063..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/unittest_three_test.py +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index dc5798306bb6..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_pytest.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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 deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 45c88355be9d..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -# 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 deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/test_root.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 129bc168f0d5..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_another_pytest.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100644 index dc5798306bb6..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_pytest.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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 deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index ad89d873e879..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_two.py +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 507e6af02063..000000000000 --- a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/unittest_three_test.py +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index da4614982080..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/elseBlocks2.py +++ /dev/null @@ -1,365 +0,0 @@ -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 deleted file mode 100644 index c8213d6c4c12..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/elseBlocks4.py +++ /dev/null @@ -1,351 +0,0 @@ -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 deleted file mode 100644 index b99c1738d297..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine2.py +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 64ad7dfb7e1a..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine4.py +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 39cea5e8caf5..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLineTab.py +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index e92233ea9ba0..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/elseBlocksTab.py +++ /dev/null @@ -1,351 +0,0 @@ -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 deleted file mode 100644 index 504feeeb3ca2..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/tryBlocks2.py +++ /dev/null @@ -1,208 +0,0 @@ - -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 deleted file mode 100644 index ce9e444cabbf..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/tryBlocks4.py +++ /dev/null @@ -1,208 +0,0 @@ - -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 deleted file mode 100644 index d94e057d493e..000000000000 --- a/src/test/pythonFiles/typeFormatFiles/tryBlocksTab.py +++ /dev/null @@ -1,208 +0,0 @@ - -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/datascience/simple_nb.ipynb b/src/test/python_files/datascience/simple_nb.ipynb similarity index 100% rename from src/test/pythonFiles/datascience/simple_nb.ipynb rename to src/test/python_files/datascience/simple_nb.ipynb diff --git a/src/test/pythonFiles/datascience/simple_note_book.py b/src/test/python_files/datascience/simple_note_book.py similarity index 100% rename from src/test/pythonFiles/datascience/simple_note_book.py rename to src/test/python_files/datascience/simple_note_book.py diff --git a/src/test/pythonFiles/debugging/forever.py b/src/test/python_files/debugging/forever.py similarity index 100% rename from src/test/pythonFiles/debugging/forever.py rename to src/test/python_files/debugging/forever.py diff --git a/src/test/pythonFiles/debugging/logMessage.py b/src/test/python_files/debugging/logMessage.py similarity index 100% rename from src/test/pythonFiles/debugging/logMessage.py rename to src/test/python_files/debugging/logMessage.py diff --git a/src/test/pythonFiles/debugging/loopyTest.py b/src/test/python_files/debugging/loopyTest.py similarity index 100% rename from src/test/pythonFiles/debugging/loopyTest.py rename to src/test/python_files/debugging/loopyTest.py diff --git a/src/test/pythonFiles/debugging/multiThread.py b/src/test/python_files/debugging/multiThread.py similarity index 100% rename from src/test/pythonFiles/debugging/multiThread.py rename to src/test/python_files/debugging/multiThread.py diff --git a/src/test/pythonFiles/debugging/printSysArgv.py b/src/test/python_files/debugging/printSysArgv.py similarity index 100% rename from src/test/pythonFiles/debugging/printSysArgv.py rename to src/test/python_files/debugging/printSysArgv.py diff --git a/src/test/pythonFiles/debugging/sample2.py b/src/test/python_files/debugging/sample2.py similarity index 100% rename from src/test/pythonFiles/debugging/sample2.py rename to src/test/python_files/debugging/sample2.py diff --git a/src/test/pythonFiles/debugging/sample2WithoutSleep.py b/src/test/python_files/debugging/sample2WithoutSleep.py similarity index 100% rename from src/test/pythonFiles/debugging/sample2WithoutSleep.py rename to src/test/python_files/debugging/sample2WithoutSleep.py diff --git a/src/test/pythonFiles/debugging/sample3WithEx.py b/src/test/python_files/debugging/sample3WithEx.py similarity index 100% rename from src/test/pythonFiles/debugging/sample3WithEx.py rename to src/test/python_files/debugging/sample3WithEx.py diff --git a/src/test/pythonFiles/debugging/sampleWithAssertEx.py b/src/test/python_files/debugging/sampleWithAssertEx.py similarity index 100% rename from src/test/pythonFiles/debugging/sampleWithAssertEx.py rename to src/test/python_files/debugging/sampleWithAssertEx.py diff --git a/src/test/pythonFiles/debugging/sampleWithSleep.py b/src/test/python_files/debugging/sampleWithSleep.py similarity index 100% rename from src/test/pythonFiles/debugging/sampleWithSleep.py rename to src/test/python_files/debugging/sampleWithSleep.py diff --git a/src/test/pythonFiles/debugging/simplePrint.py b/src/test/python_files/debugging/simplePrint.py similarity index 100% rename from src/test/pythonFiles/debugging/simplePrint.py rename to src/test/python_files/debugging/simplePrint.py diff --git a/src/test/pythonFiles/debugging/stackFrame.py b/src/test/python_files/debugging/stackFrame.py similarity index 100% rename from src/test/pythonFiles/debugging/stackFrame.py rename to src/test/python_files/debugging/stackFrame.py diff --git a/src/test/pythonFiles/debugging/startAndWait.py b/src/test/python_files/debugging/startAndWait.py similarity index 100% rename from src/test/pythonFiles/debugging/startAndWait.py rename to src/test/python_files/debugging/startAndWait.py diff --git a/src/test/pythonFiles/debugging/stdErrOutput.py b/src/test/python_files/debugging/stdErrOutput.py similarity index 100% rename from src/test/pythonFiles/debugging/stdErrOutput.py rename to src/test/python_files/debugging/stdErrOutput.py diff --git a/src/test/pythonFiles/debugging/stdOutOutput.py b/src/test/python_files/debugging/stdOutOutput.py similarity index 100% rename from src/test/pythonFiles/debugging/stdOutOutput.py rename to src/test/python_files/debugging/stdOutOutput.py diff --git a/src/test/pythonFiles/debugging/wait_for_file.py b/src/test/python_files/debugging/wait_for_file.py similarity index 100% rename from src/test/pythonFiles/debugging/wait_for_file.py rename to src/test/python_files/debugging/wait_for_file.py diff --git a/src/test/python_files/dummy.py b/src/test/python_files/dummy.py new file mode 100644 index 000000000000..10f13768abe0 --- /dev/null +++ b/src/test/python_files/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/python_files/environments/conda/Scripts/conda.exe similarity index 100% rename from src/test/pythonFiles/environments/conda/Scripts/conda.exe rename to src/test/python_files/environments/conda/Scripts/conda.exe diff --git a/src/test/python_files/environments/conda/bin/python b/src/test/python_files/environments/conda/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/conda/envs/numpy/bin/python b/src/test/python_files/environments/conda/envs/numpy/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/conda/envs/numpy/python.exe b/src/test/python_files/environments/conda/envs/numpy/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/conda/envs/scipy/bin/python b/src/test/python_files/environments/conda/envs/scipy/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/conda/envs/scipy/python.exe b/src/test/python_files/environments/conda/envs/scipy/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/path1/one b/src/test/python_files/environments/path1/one new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/path1/one.exe b/src/test/python_files/environments/path1/one.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/path1/python.exe b/src/test/python_files/environments/path1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/path2/one b/src/test/python_files/environments/path2/one new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/path2/one.exe b/src/test/python_files/environments/path2/one.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/environments/path2/python.exe b/src/test/python_files/environments/path2/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/intellisense/test.py b/src/test/python_files/intellisense/test.py new file mode 100644 index 000000000000..5b3dac8e7b38 --- /dev/null +++ b/src/test/python_files/intellisense/test.py @@ -0,0 +1 @@ +def syntaxerror \ No newline at end of file diff --git a/src/test/pythonFiles/shebang/plain.py b/src/test/python_files/shebang/plain.py similarity index 100% rename from src/test/pythonFiles/shebang/plain.py rename to src/test/python_files/shebang/plain.py diff --git a/src/test/pythonFiles/shebang/shebang.py b/src/test/python_files/shebang/shebang.py similarity index 100% rename from src/test/pythonFiles/shebang/shebang.py rename to src/test/python_files/shebang/shebang.py diff --git a/src/test/pythonFiles/shebang/shebangEnv.py b/src/test/python_files/shebang/shebangEnv.py similarity index 100% rename from src/test/pythonFiles/shebang/shebangEnv.py rename to src/test/python_files/shebang/shebangEnv.py diff --git a/src/test/pythonFiles/shebang/shebangInvalid.py b/src/test/python_files/shebang/shebangInvalid.py similarity index 100% rename from src/test/pythonFiles/shebang/shebangInvalid.py rename to src/test/python_files/shebang/shebangInvalid.py diff --git a/src/test/python_files/tensorBoard/noMatch.py b/src/test/python_files/tensorBoard/noMatch.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/python_files/tensorBoard/sourcefile.py b/src/test/python_files/tensorBoard/sourcefile.py new file mode 100644 index 000000000000..dfcacad27fac --- /dev/null +++ b/src/test/python_files/tensorBoard/sourcefile.py @@ -0,0 +1 @@ +from torch.utils.tensorboard import SummaryWriter diff --git a/src/test/python_files/tensorBoard/tensorboard_import.ipynb b/src/test/python_files/tensorBoard/tensorboard_import.ipynb new file mode 100644 index 000000000000..1748c9563480 --- /dev/null +++ b/src/test/python_files/tensorBoard/tensorboard_import.ipynb @@ -0,0 +1,27 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tensorboard" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python", + "nbconvert_exporter": "python", + "version": "3.8.6-final" + }, + "orig_nbformat": 2 + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/src/test/python_files/tensorBoard/tensorboard_imports.py b/src/test/python_files/tensorBoard/tensorboard_imports.py new file mode 100644 index 000000000000..dfcacad27fac --- /dev/null +++ b/src/test/python_files/tensorBoard/tensorboard_imports.py @@ -0,0 +1 @@ +from torch.utils.tensorboard import SummaryWriter diff --git a/src/test/python_files/tensorBoard/tensorboard_launch.py b/src/test/python_files/tensorBoard/tensorboard_launch.py new file mode 100644 index 000000000000..dc6b2ada9bbe --- /dev/null +++ b/src/test/python_files/tensorBoard/tensorboard_launch.py @@ -0,0 +1,2 @@ +%load_ext tensorboard +%tensorboard --logdir logs/fit diff --git a/src/test/python_files/tensorBoard/tensorboard_nbextension.ipynb b/src/test/python_files/tensorBoard/tensorboard_nbextension.ipynb new file mode 100644 index 000000000000..5352ecc70f77 --- /dev/null +++ b/src/test/python_files/tensorBoard/tensorboard_nbextension.ipynb @@ -0,0 +1,31 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext tensorboard" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%tensorboard --logdir logs/fit" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "orig_nbformat": 2 + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/src/test/pythonFiles/terminalExec/sample1_normalized.py b/src/test/python_files/terminalExec/sample1_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample1_normalized.py rename to src/test/python_files/terminalExec/sample1_normalized.py diff --git a/src/test/python_files/terminalExec/sample1_normalized_selection.py b/src/test/python_files/terminalExec/sample1_normalized_selection.py new file mode 100644 index 000000000000..da19fd10f41e --- /dev/null +++ b/src/test/python_files/terminalExec/sample1_normalized_selection.py @@ -0,0 +1,22 @@ +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/python_files/terminalExec/sample1_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample1_raw.py rename to src/test/python_files/terminalExec/sample1_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample2_normalized.py b/src/test/python_files/terminalExec/sample2_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample2_normalized.py rename to src/test/python_files/terminalExec/sample2_normalized.py diff --git a/src/test/python_files/terminalExec/sample2_normalized_selection.py b/src/test/python_files/terminalExec/sample2_normalized_selection.py new file mode 100644 index 000000000000..a333d4e0daae --- /dev/null +++ b/src/test/python_files/terminalExec/sample2_normalized_selection.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/python_files/terminalExec/sample2_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample2_raw.py rename to src/test/python_files/terminalExec/sample2_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample3_normalized.py b/src/test/python_files/terminalExec/sample3_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample3_normalized.py rename to src/test/python_files/terminalExec/sample3_normalized.py diff --git a/src/test/python_files/terminalExec/sample3_normalized_selection.py b/src/test/python_files/terminalExec/sample3_normalized_selection.py new file mode 100644 index 000000000000..4fa62091c66d --- /dev/null +++ b/src/test/python_files/terminalExec/sample3_normalized_selection.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/python_files/terminalExec/sample3_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample3_raw.py rename to src/test/python_files/terminalExec/sample3_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample4_normalized.py b/src/test/python_files/terminalExec/sample4_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample4_normalized.py rename to src/test/python_files/terminalExec/sample4_normalized.py diff --git a/src/test/python_files/terminalExec/sample4_normalized_selection.py b/src/test/python_files/terminalExec/sample4_normalized_selection.py new file mode 100644 index 000000000000..359da8b2d6a4 --- /dev/null +++ b/src/test/python_files/terminalExec/sample4_normalized_selection.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/sample4_raw.py b/src/test/python_files/terminalExec/sample4_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample4_raw.py rename to src/test/python_files/terminalExec/sample4_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample5_normalized.py b/src/test/python_files/terminalExec/sample5_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample5_normalized.py rename to src/test/python_files/terminalExec/sample5_normalized.py diff --git a/src/test/python_files/terminalExec/sample5_normalized_selection.py b/src/test/python_files/terminalExec/sample5_normalized_selection.py new file mode 100644 index 000000000000..c71a15aa5dd7 --- /dev/null +++ b/src/test/python_files/terminalExec/sample5_normalized_selection.py @@ -0,0 +1,9 @@ +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/python_files/terminalExec/sample5_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample5_raw.py rename to src/test/python_files/terminalExec/sample5_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample6_normalized.py b/src/test/python_files/terminalExec/sample6_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample6_normalized.py rename to src/test/python_files/terminalExec/sample6_normalized.py diff --git a/src/test/python_files/terminalExec/sample6_normalized_selection.py b/src/test/python_files/terminalExec/sample6_normalized_selection.py new file mode 100644 index 000000000000..ad7a11004cba --- /dev/null +++ b/src/test/python_files/terminalExec/sample6_normalized_selection.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/python_files/terminalExec/sample6_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample6_raw.py rename to src/test/python_files/terminalExec/sample6_raw.py diff --git a/src/test/pythonFiles/terminalExec/sample7_normalized.py b/src/test/python_files/terminalExec/sample7_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample7_normalized.py rename to src/test/python_files/terminalExec/sample7_normalized.py diff --git a/src/test/pythonFiles/terminalExec/sample8_normalized.py b/src/test/python_files/terminalExec/sample7_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample8_normalized.py rename to src/test/python_files/terminalExec/sample7_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample7_raw.py b/src/test/python_files/terminalExec/sample7_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample7_raw.py rename to src/test/python_files/terminalExec/sample7_raw.py diff --git a/src/test/python_files/terminalExec/sample8_normalized.py b/src/test/python_files/terminalExec/sample8_normalized.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/python_files/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/python_files/terminalExec/sample8_normalized_selection.py b/src/test/python_files/terminalExec/sample8_normalized_selection.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/python_files/terminalExec/sample8_normalized_selection.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/python_files/terminalExec/sample8_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample8_raw.py rename to src/test/python_files/terminalExec/sample8_raw.py diff --git a/src/test/python_files/terminalExec/sample_invalid_smart_selection.py b/src/test/python_files/terminalExec/sample_invalid_smart_selection.py new file mode 100644 index 000000000000..73d9e0fba066 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_invalid_smart_selection.py @@ -0,0 +1,10 @@ +def beliebig(x, y, *mehr): + print "x=", x, ", x=", y + print "mehr: ", mehr + +list = [ +1, +2, +3, +] +print("Above is invalid");print("deprecated");print("show warning") diff --git a/src/test/pythonFiles/terminalExec/sample_normalized.py b/src/test/python_files/terminalExec/sample_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample_normalized.py rename to src/test/python_files/terminalExec/sample_normalized.py diff --git a/src/test/python_files/terminalExec/sample_normalized_selection.py b/src/test/python_files/terminalExec/sample_normalized_selection.py new file mode 100644 index 000000000000..8ee9b90cdd27 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_normalized_selection.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/python_files/terminalExec/sample_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample_raw.py rename to src/test/python_files/terminalExec/sample_raw.py diff --git a/src/test/python_files/terminalExec/sample_smart_selection.py b/src/test/python_files/terminalExec/sample_smart_selection.py new file mode 100644 index 000000000000..3933f06b5d65 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_smart_selection.py @@ -0,0 +1,21 @@ +my_dict = { + "key1": "value1", + "key2": "value2" +} +#Sample + +print("Audi");print("BMW");print("Mercedes") + +# print("dont print me") + +def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + +# Skip me to prove that you did a good job +def next_func(): + print("You") + diff --git a/src/test/refactor/extension.refactor.extract.method.test.ts b/src/test/refactor/extension.refactor.extract.method.test.ts deleted file mode 100644 index 48c51428673a..000000000000 --- a/src/test/refactor/extension.refactor.extract.method.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -// 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 deleted file mode 100644 index dd178ffe7906..000000000000 --- a/src/test/refactor/extension.refactor.extract.var.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -// 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 deleted file mode 100644 index a192bf36dc56..000000000000 --- a/src/test/refactor/rename.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -// 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/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts new file mode 100644 index 000000000000..2cf18cefe1f7 --- /dev/null +++ b/src/test/repl/nativeRepl.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode'; +import { expect } from 'chai'; + +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import * as NativeReplModule from '../../client/repl/nativeRepl'; +import * as persistentState from '../../client/common/persistentState'; +import * as PythonServer from '../../client/repl/pythonServer'; +import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis'; +import * as replController from '../../client/repl/replController'; +import { executeCommand } from '../../client/common/vscodeApis/commandApis'; + +suite('REPL - Native REPL', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + + let disposable: TypeMoq.IMock<Disposable>; + let disposableArray: Disposable[] = []; + let setReplDirectoryStub: sinon.SinonStub; + let setReplControllerSpy: sinon.SinonSpy; + let getWorkspaceStateValueStub: sinon.SinonStub; + let updateWorkspaceStateValueStub: sinon.SinonStub; + let createReplControllerStub: sinon.SinonStub; + let mockNotebookController: any; + + setup(() => { + (NativeReplModule as any).nativeRepl = undefined; + + mockNotebookController = { + id: 'mockController', + dispose: sinon.stub(), + updateNotebookAffinity: sinon.stub(), + createNotebookCellExecution: sinon.stub(), + variableProvider: null, + }; + + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + disposable = TypeMoq.Mock.ofType<Disposable>(); + disposableArray = [disposable.object]; + + createReplControllerStub = sinon.stub(replController, 'createReplController').returns(mockNotebookController); + setReplDirectoryStub = sinon.stub(NativeReplModule.NativeRepl.prototype as any, 'setReplDirectory').resolves(); + setReplControllerSpy = sinon.spy(NativeReplModule.NativeRepl.prototype, 'setReplController'); + updateWorkspaceStateValueStub = sinon.stub(persistentState, 'updateWorkspaceStateValue').resolves(); + }); + + teardown(async () => { + disposableArray.forEach((d) => { + if (d) { + d.dispose(); + } + }); + disposableArray = []; + sinon.restore(); + executeCommand('workbench.action.closeActiveEditor'); + }); + + test('getNativeRepl should call create constructor', async () => { + const createMethodStub = sinon.stub(NativeReplModule.NativeRepl, 'create'); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + expect(createMethodStub.calledOnce).to.be.true; + }); + + test('sendToNativeRepl should look for memento URI if notebook document is undefined', async () => { + getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns(undefined); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + nativeRepl.sendToNativeRepl(undefined, false); + + expect(getWorkspaceStateValueStub.calledOnce).to.be.true; + }); + + test('sendToNativeRepl should call updateWorkspaceStateValue', async () => { + getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns('myNameIsMemento'); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + nativeRepl.sendToNativeRepl(undefined, false); + + expect(updateWorkspaceStateValueStub.calledOnce).to.be.true; + }); + + test('create should call setReplDirectory, setReplController', async () => { + const interpreter = await interpreterService.object.getActiveInterpreter(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + await NativeReplModule.NativeRepl.create(interpreter as PythonEnvironment); + + expect(setReplDirectoryStub.calledOnce).to.be.true; + expect(setReplControllerSpy.calledOnce).to.be.true; + expect(createReplControllerStub.calledOnce).to.be.true; + }); + + test('watchNotebookClosed should clean up resources when notebook is closed', async () => { + const notebookCloseEmitter = new EventEmitter<NotebookDocument>(); + sinon.stub(vscodeWorkspaceApis, 'onDidCloseNotebookDocument').callsFake((handler) => { + const disposable = notebookCloseEmitter.event(handler); + return disposable; + }); + + const mockPythonServer = { + onCodeExecuted: new EventEmitter<void>().event, + execute: sinon.stub().resolves({ status: true, output: 'test output' }), + executeSilently: sinon.stub().resolves({ status: true, output: 'test output' }), + interrupt: sinon.stub(), + input: sinon.stub(), + checkValidCommand: sinon.stub().resolves(true), + dispose: sinon.stub(), + isExecuting: false, + isDisposed: false, + }; + + // Track the number of times createPythonServer was called + let createPythonServerCallCount = 0; + sinon.stub(PythonServer, 'createPythonServer').callsFake(() => { + // eslint-disable-next-line no-plusplus + createPythonServerCallCount++; + return mockPythonServer; + }); + + const interpreter = await interpreterService.object.getActiveInterpreter(); + + // Create NativeRepl directly to have more control over its state, go around private constructor. + const nativeRepl = new (NativeReplModule.NativeRepl as any)(); + nativeRepl.interpreter = interpreter as PythonEnvironment; + nativeRepl.cwd = '/helloJustMockedCwd/cwd'; + nativeRepl.pythonServer = mockPythonServer; + nativeRepl.replController = mockNotebookController; + nativeRepl.disposables = []; + + // Make the singleton point to our instance for testing + // Otherwise, it gets mixed with Native Repl from .create from test above. + (NativeReplModule as any).nativeRepl = nativeRepl; + + // Reset call count after initial setup + createPythonServerCallCount = 0; + + // Set notebookDocument to a mock document + const mockReplUri = Uri.parse('untitled:Untitled-999.ipynb?jupyter-notebook'); + const mockNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + nativeRepl.notebookDocument = mockNotebookDocument; + + // Create a mock notebook document for closing event with same URI + const closingNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + notebookCloseEmitter.fire(closingNotebookDocument); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect( + updateWorkspaceStateValueStub.calledWith(NativeReplModule.NATIVE_REPL_URI_MEMENTO, undefined), + 'updateWorkspaceStateValue should be called with NATIVE_REPL_URI_MEMENTO and undefined', + ).to.be.true; + expect(mockPythonServer.dispose.calledOnce, 'pythonServer.dispose() should be called once').to.be.true; + expect(createPythonServerCallCount, 'createPythonServer should be called to create a new server').to.equal(1); + expect(nativeRepl.notebookDocument, 'notebookDocument should be undefined after closing').to.be.undefined; + expect(nativeRepl.newReplSession, 'newReplSession should be set to true after closing').to.be.true; + expect(mockNotebookController.dispose.calledOnce, 'replController.dispose() should be called once').to.be.true; + }); +}); diff --git a/src/test/repl/replCommand.test.ts b/src/test/repl/replCommand.test.ts new file mode 100644 index 000000000000..0b5edda863f9 --- /dev/null +++ b/src/test/repl/replCommand.test.ts @@ -0,0 +1,250 @@ +// Create test suite and test cases for the `replUtils` module +import * as TypeMoq from 'typemoq'; +import { commands, Disposable, Uri } from 'vscode'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ICommandManager } from '../../client/common/application/types'; +import { ICodeExecutionHelper } from '../../client/terminals/types'; +import * as replCommands from '../../client/repl/replCommands'; +import * as replUtils from '../../client/repl/replUtils'; +import * as nativeRepl from '../../client/repl/nativeRepl'; +import * as windowApis from '../../client/common/vscodeApis/windowApis'; +import { Commands } from '../../client/common/constants'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; + +suite('REPL - register native repl command', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let executionHelper: TypeMoq.IMock<ICodeExecutionHelper>; + let getSendToNativeREPLSettingStub: sinon.SinonStub; + // @ts-ignore: TS6133 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let registerCommandSpy: sinon.SinonSpy; + let executeInTerminalStub: sinon.SinonStub; + let getNativeReplStub: sinon.SinonStub; + let disposable: TypeMoq.IMock<Disposable>; + let disposableArray: Disposable[] = []; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + executionHelper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + commandManager + .setup((cm) => cm.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => TypeMoq.Mock.ofType<Disposable>().object); + + getSendToNativeREPLSettingStub = sinon.stub(replUtils, 'getSendToNativeREPLSetting'); + getSendToNativeREPLSettingStub.returns(false); + executeInTerminalStub = sinon.stub(replUtils, 'executeInTerminal'); + executeInTerminalStub.returns(Promise.resolve()); + registerCommandSpy = sinon.spy(commandManager.object, 'registerCommand'); + disposable = TypeMoq.Mock.ofType<Disposable>(); + disposableArray = [disposable.object]; + }); + + teardown(() => { + sinon.restore(); + disposableArray.forEach((d) => { + if (d) { + d.dispose(); + } + }); + + disposableArray = []; + }); + + test('Ensure repl command is registered', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + await replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + commandManager.verify( + (c) => c.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), + ); + }); + + test('Ensure getSendToNativeREPLSetting is called', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + let commandHandler: undefined | (() => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!(); + + sinon.assert.calledOnce(getSendToNativeREPLSettingStub); + }); + + test('Ensure executeInTerminal is called when getSendToNativeREPLSetting returns false', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + getSendToNativeREPLSettingStub.returns(false); + + let commandHandler: undefined | (() => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!(); + + sinon.assert.calledOnce(executeInTerminalStub); + }); + + test('Ensure we call getNativeREPL() when interpreter exist', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + getSendToNativeREPLSettingStub.returns(true); + getNativeReplStub = sinon.stub(nativeRepl, 'getNativeRepl'); + + let commandHandler: undefined | ((uri: string) => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!('uri'); + sinon.assert.calledOnce(getNativeReplStub); + }); + + test('Ensure we do not call getNativeREPL() when interpreter does not exist', async () => { + getNativeReplStub = sinon.stub(nativeRepl, 'getNativeRepl'); + getSendToNativeREPLSettingStub.returns(true); + + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + let commandHandler: undefined | ((uri: string) => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!('uri'); + sinon.assert.notCalled(getNativeReplStub); + }); +}); + +suite('Native REPL getActiveInterpreter', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let executeCommandStub: sinon.SinonStub; + let getActiveResourceStub: sinon.SinonStub; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + executeCommandStub = sinon.stub(commands, 'executeCommand').resolves(undefined); + getActiveResourceStub = sinon.stub(windowApis, 'getActiveResource'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Uses active resource when uri is undefined', async () => { + const resource = Uri.file('/workspace/app.py'); + const expected = ({ path: 'ps' } as unknown) as PythonEnvironment; + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(expected)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(expected); + interpreterService.verify((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource)), TypeMoq.Times.once()); + sinon.assert.notCalled(executeCommandStub); + }); + + test('Triggers environment selection using active resource when interpreter is missing', async () => { + const resource = Uri.file('/workspace/app.py'); + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(undefined)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(undefined); + sinon.assert.calledWith(executeCommandStub, Commands.TriggerEnvironmentSelection, resource); + }); +}); diff --git a/src/test/repl/variableProvider.test.ts b/src/test/repl/variableProvider.test.ts new file mode 100644 index 000000000000..e401041e17d9 --- /dev/null +++ b/src/test/repl/variableProvider.test.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import sinon from 'sinon'; +import { + NotebookDocument, + CancellationTokenSource, + VariablesResult, + Variable, + EventEmitter, + ConfigurationScope, + WorkspaceConfiguration, +} from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IVariableDescription } from '../../client/repl/variables/types'; +import { VariablesProvider } from '../../client/repl/variables/variablesProvider'; +import { VariableRequester } from '../../client/repl/variables/variableRequester'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('ReplVariablesProvider', () => { + let provider: VariablesProvider; + let varRequester: TypeMoq.IMock<VariableRequester>; + let notebook: TypeMoq.IMock<NotebookDocument>; + let getConfigurationStub: sinon.SinonStub; + let configMock: TypeMoq.IMock<WorkspaceConfiguration>; + let enabled: boolean; + const executionEventEmitter = new EventEmitter<void>(); + const cancellationToken = new CancellationTokenSource().token; + + const objectVariable: IVariableDescription = { + name: 'myObject', + value: '...', + root: 'myObject', + hasNamedChildren: true, + propertyChain: [], + }; + + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 3, + root: 'myObject', + propertyChain: ['myList'], + }; + + function createListItem(index: number): IVariableDescription { + return { + name: index.toString(), + value: `value${index}`, + count: index, + root: 'myObject', + propertyChain: ['myList', index], + }; + } + + function setVariablesForParent( + parent: IVariableDescription | undefined, + result: IVariableDescription[], + updated?: IVariableDescription[], + startIndex?: number, + ) { + let returnedOnce = false; + varRequester + .setup((v) => v.getAllVariableDescriptions(parent, startIndex ?? TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + if (updated && returnedOnce) { + return Promise.resolve(updated); + } + returnedOnce = true; + return Promise.resolve(result); + }); + } + + async function provideVariables(parent: Variable | undefined, kind = 1) { + const results: VariablesResult[] = []; + for await (const result of provider.provideVariables(notebook.object, parent, kind, 0, cancellationToken)) { + results.push(result); + } + return results; + } + + setup(() => { + enabled = true; + varRequester = TypeMoq.Mock.ofType<VariableRequester>(); + notebook = TypeMoq.Mock.ofType<NotebookDocument>(); + provider = new VariablesProvider(varRequester.object, () => notebook.object, executionEventEmitter.event); + configMock = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + configMock.setup((c) => c.get<boolean>('REPL.provideVariables')).returns(() => enabled); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provideVariables without parent should yield variables', async () => { + setVariablesForParent(undefined, [objectVariable]); + + const results = await provideVariables(undefined); + + assert.isNotEmpty(results); + assert.equal(results.length, 1); + assert.equal(results[0].variable.name, 'myObject'); + assert.equal(results[0].variable.expression, 'myObject'); + }); + + test('No variables are returned when variable provider is disabled', async () => { + enabled = false; + setVariablesForParent(undefined, [objectVariable]); + + const results = await provideVariables(undefined); + + assert.isEmpty(results); + }); + + test('No change event from provider when disabled', async () => { + enabled = false; + let eventFired = false; + provider.onDidChangeVariables(() => { + eventFired = true; + }); + + executionEventEmitter.fire(); + + assert.isFalse(eventFired, 'event should not have fired'); + }); + + test('Variables change event from provider should fire when execution happens', async () => { + let eventFired = false; + provider.onDidChangeVariables(() => { + eventFired = true; + }); + + executionEventEmitter.fire(); + + assert.isTrue(eventFired, 'event should have fired'); + }); + + test('provideVariables with a parent should call get children correctly', async () => { + const listVariableItems = [0, 1, 2].map(createListItem); + setVariablesForParent(undefined, [objectVariable]); + + // pass each the result as the parent in the next call + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, [listVariable]); + const listResult = (await provideVariables(rootVariable!.variable))[0]; + setVariablesForParent(listResult.variable as IVariableDescription, listVariableItems); + const listItems = await provideVariables(listResult!.variable, 2); + + assert.equal(listResult.variable.name, 'myList'); + assert.equal(listResult.variable.expression, 'myObject.myList'); + assert.isNotEmpty(listItems); + assert.equal(listItems.length, 3); + listItems.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + assert.equal(item.variable.expression, `myObject.myList[${index}]`); + }); + }); + + test('All indexed variables should be returned when requested', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting less indexed items than the specified count is handled', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + setVariablesForParent(rootVariable.variable as IVariableDescription, [], undefined, 5); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 5); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting variables again with new execution count should get updated variables', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + setVariablesForParent(undefined, [intVariable], [{ ...intVariable, value: '2' }]); + + const first = await provideVariables(undefined); + executionEventEmitter.fire(); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + assert.equal(second[0].variable.value, '2'); + }); + + test('Getting variables again with same execution count should not make another call', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + + setVariablesForParent(undefined, [intVariable]); + + const first = await provideVariables(undefined); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('Cache pages of indexed children correctly', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + + await provideVariables(rootVariable!.variable, 2); + + // once for the parent and once for each of the two pages of list items + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + + // no extra calls for getting the children again + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); + }); +}); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index c7b29c7e14a7..382659b3f838 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -1,30 +1,25 @@ // 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 { anything } 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 { Disposable, Memento } from 'vscode'; +import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; import { PlatformService } from '../client/common/platform/platformService'; +import { isWindows } from '../client/common/utils/platform'; 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 { IFileSystem, IPlatformService, IRegistry } from '../client/common/platform/types'; 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 + IPythonToolExecutionService, } from '../client/common/process/types'; import { registerTypes as commonRegisterTypes } from '../client/common/serviceRegistry'; import { @@ -32,19 +27,17 @@ import { ICurrentProcess, IDisposableRegistry, IMemento, - IOutputChannel, IPathUtils, IsWindows, - WORKSPACE_MEMENTO + WORKSPACE_MEMENTO, + ILogOutputChannel, } 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 + IInterpreterAutoSelectionProxyService, } from '../client/interpreter/autoSelection/types'; import { IInterpreterService } from '../client/interpreter/contracts'; import { InterpreterService } from '../client/interpreter/interpreterService'; @@ -52,115 +45,15 @@ 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 { LegacyFileSystem } from './legacyFileSystem'; 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); - } -} +import { registerForIOC } from './pythonEnvironments/legacyIOC'; +import { createTypeMoq } from './mocks/helper'; export class IocContainer { // This may be set (before any registration happens) to indicate @@ -170,12 +63,13 @@ export class IocContainer { public useVSCodeAPI = true; public readonly serviceManager: IServiceManager; + public readonly serviceContainer: IServiceContainer; private disposables: Disposable[] = []; constructor() { - const cont = new Container(); + const cont = new Container({ skipBaseClassChecks: true }); this.serviceManager = new ServiceManager(cont); this.serviceContainer = new ServiceContainer(cont); @@ -186,124 +80,112 @@ export class IocContainer { const stdOutputChannel = new MockOutputChannel('Python'); this.disposables.push(stdOutputChannel); - this.serviceManager.addSingletonInstance<OutputChannel>( - IOutputChannel, - stdOutputChannel, - STANDARD_OUTPUT_CHANNEL - ); + this.serviceManager.addSingletonInstance<ILogOutputChannel>(ILogOutputChannel, stdOutputChannel); const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); - this.serviceManager.addSingletonInstance<OutputChannel>(IOutputChannel, testOutputChannel, TEST_OUTPUT_CHANNEL); + this.serviceManager.addSingletonInstance<ILogOutputChannel>(ILogOutputChannel, testOutputChannel); this.serviceManager.addSingleton<IInterpreterAutoSelectionService>( IInterpreterAutoSelectionService, - MockAutoSelectionService + MockAutoSelectionService, ); - this.serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( - IInterpreterAutoSeletionProxyService, - MockAutoSelectionService + this.serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + 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; + if (disposable) { + const promise = disposable.dispose() as Promise<unknown>; + if (promise) { + await promise; + } } } this.disposables = []; this.serviceManager.dispose(); } - public registerCommonTypes(registerFileSystem: boolean = true) { + public registerCommonTypes(registerFileSystem = true): void { commonRegisterTypes(this.serviceManager); if (registerFileSystem) { this.registerFileSystemTypes(); } } - public registerFileSystemTypes() { + + public registerFileSystemTypes(): void { this.serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); this.serviceManager.addSingleton<IFileSystem>( IFileSystem, // Maybe use fake vscode.workspace.filesystem API: - this.useVSCodeAPI ? FileSystem : LegacyFileSystem + this.useVSCodeAPI ? FileSystem : LegacyFileSystem, ); } - public registerProcessTypes() { + + public registerProcessTypes(): void { 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) - ); + const mockEnvironmentActivationService = createTypeMoq<IEnvironmentActivationService>(); + mockEnvironmentActivationService + .setup((f) => f.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve(undefined)); } - public registerVariableTypes() { + + public registerVariableTypes(): void { variableRegisterTypes(this.serviceManager); } - public registerUnitTestTypes() { + + public registerUnitTestTypes(): void { unittestsRegisterTypes(this.serviceManager); } - public registerLinterTypes() { - lintersRegisterTypes(this.serviceManager); - } - public registerFormatterTypes() { - formattersRegisterTypes(this.serviceManager); - } - public registerPlatformTypes() { + + public registerPlatformTypes(): void { platformRegisterTypes(this.serviceManager); } - public registerInterpreterTypes() { - // This method registers all interpreter types except `IInterpreterAutoSeletionProxyService` & `IEnvironmentActivationService`, as it's already registered in the constructor & registerMockProcessTypes() respectively + + public registerInterpreterTypes(): void { + // This method registers all interpreter types except `IInterpreterAutoSelectionProxyService` & `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)); + + public registerMockProcessTypes(): void { + const processServiceFactory = createTypeMoq<IProcessServiceFactory>(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const processService = new MockProcessService(new ProcessService(process.env as any)); processServiceFactory.setup((f) => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService)); this.serviceManager.addSingletonInstance<IProcessServiceFactory>( IProcessServiceFactory, - processServiceFactory.object + processServiceFactory.object, ); this.serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory); this.serviceManager.addSingleton<IPythonToolExecutionService>( IPythonToolExecutionService, - PythonToolExecutionService + PythonToolExecutionService, ); this.serviceManager.addSingleton<IEnvironmentActivationService>( IEnvironmentActivationService, - EnvironmentActivationService + EnvironmentActivationService, ); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); - when( - mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything(), anything()) - ).thenResolve(); + const mockEnvironmentActivationService = createTypeMoq<IEnvironmentActivationService>(); + mockEnvironmentActivationService + .setup((m) => m.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve(undefined)); this.serviceManager.rebindInstance<IEnvironmentActivationService>( IEnvironmentActivationService, - instance(mockEnvironmentActivationService) + mockEnvironmentActivationService.object, ); } - public registerMockInterpreterTypes() { + public async registerMockInterpreterTypes(): Promise<void> { this.serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); this.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); - registerForIOC(this.serviceManager); + await registerForIOC(this.serviceManager, this.serviceContainer); } - public registerMockProcess() { - this.serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); + public registerMockProcess(): void { + this.serviceManager.addSingletonInstance<boolean>(IsWindows, isWindows()); 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 index 4c8fa12b97cd..5f5b691fb496 100644 --- a/src/test/smoke/common.ts +++ b/src/test/smoke/common.ts @@ -3,57 +3,106 @@ '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 * as fs from '../../client/common/platform/fs-paths'; +import { JUPYTER_EXTENSION_ID } from '../../client/common/constants'; import { SMOKE_TEST_EXTENSIONS_DIR } from '../constants'; import { noop, sleep } from '../core'; -export async function updateSetting(setting: string, value: any) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any +export async function updateSetting(setting: string, value: any): Promise<void> { const resource = vscode.workspace.workspaceFolders![0].uri; await vscode.workspace .getConfiguration('python', resource) .update(setting, value, vscode.ConfigurationTarget.WorkspaceFolder); } -export async function removeLanguageServerFiles() { +export async function removeLanguageServerFiles(): Promise<void> { 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))); + glob.default('languageServer.*', { cwd: SMOKE_TEST_EXTENSIONS_DIR }, (ex, matches) => { + if (ex) { + reject(ex); + } else { + resolve(matches.map((item) => path.join(SMOKE_TEST_EXTENSIONS_DIR, item))); + } }); }); } -export function isJediEnabled() { +export function isJediEnabled(): boolean { 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) { +export async function enableJedi(enable: boolean | undefined): Promise<void> { 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'); + +export async function openNotebook(file: string): Promise<vscode.NotebookDocument> { + await verifyExtensionIsAvailable(JUPYTER_EXTENSION_ID); + await vscode.commands.executeCommand('vscode.openWith', vscode.Uri.file(file), 'jupyter-notebook'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const notebook = (vscode.window.activeTextEditor!.document as any | undefined)?.notebook as vscode.NotebookDocument; + assert.ok(notebook, 'Notebook did not open'); + return notebook; +} + +export async function openNotebookAndWaitForLS(file: string): Promise<vscode.NotebookDocument> { + const notebook = await openNotebook(file); // 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) + notebook.cellAt(0).document.uri, + new vscode.Position(0, 0), ); // For for LS to get extracted. await sleep(10_000); + return notebook; +} + +export async function openFileAndWaitForLS(file: string): Promise<vscode.TextDocument> { + const textDocument = await vscode.workspace.openTextDocument(file).then( + (result) => result, + (err) => { + assert.fail(`Something went wrong opening the text document: ${err}`); + }, + ); + await vscode.window.showTextDocument(textDocument).then(undefined, (err) => { + assert.fail(`Something went wrong showing the text document: ${err}`); + }); + assert.ok(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.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + new vscode.Position(0, 0), + ) + .then(undefined, (err) => { + assert.fail(`Something went wrong opening the file: ${err}`); + }); + // For for LS to get extracted. + await sleep(10_000); return textDocument; } + +export async function verifyExtensionIsAvailable(extensionId: string): Promise<void> { + const extension = vscode.extensions.all.find((e) => e.id === extensionId); + assert.ok( + extension, + `Extension ${extensionId} not installed. ${JSON.stringify(vscode.extensions.all.map((e) => e.id))}`, + ); + await extension.activate(); +} diff --git a/src/test/smoke/datascience.smoke.test.ts b/src/test/smoke/datascience.smoke.test.ts index 0712239654e5..9f4421de4676 100644 --- a/src/test/smoke/datascience.smoke.test.ts +++ b/src/test/smoke/datascience.smoke.test.ts @@ -3,25 +3,28 @@ 'use strict'; -// tslint:disable:max-func-body-length no-invalid-this no-any - -import * as fs from 'fs-extra'; +import * as assert from 'assert'; 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'; +import * as fs from '../../client/common/platform/fs-paths'; +import { openFile, waitForCondition } from '../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; +import { sleep } from '../core'; +import { closeActiveWindows, initializeTest } from '../initialize'; const timeoutForCellToRun = 3 * 60 * 1_000; -suite('Smoke Test: Interactive Window', () => { +suite('Smoke Test: Datascience', () => { suiteSetup(async function () { - if (!IS_SMOKE_TEST) { - return this.skip(); - } - await initialize(); - await setAutoSaveDelayInWorkspaceRoot(1); + return this.skip(); + // if (!IS_SMOKE_TEST) { + // return this.skip(); + // } + // await verifyExtensionIsAvailable(JUPYTER_EXTENSION_ID); + // await initialize(); + // await setAutoSaveDelayInWorkspaceRoot(1); + + // return undefined; }); setup(initializeTest); suiteTeardown(closeActiveWindows); @@ -32,9 +35,9 @@ suite('Smoke Test: Interactive Window', () => { EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', - 'pythonFiles', + 'python_files', 'datascience', - 'simple_note_book.py' + 'simple_note_book.py', ); const outputFile = path.join(path.dirname(file), 'ds.log'); if (await fs.pathExists(outputFile)) { @@ -45,7 +48,9 @@ suite('Smoke Test: Interactive Window', () => { // Wait for code lenses to get detected. await sleep(1_000); - await vscode.commands.executeCommand<void>('python.datascience.runallcells', textDocument.uri); + await vscode.commands.executeCommand<void>('jupyter.runallcells', textDocument.uri).then(undefined, (err) => { + assert.fail(`Something went wrong running all cells in the interactive window: ${err}`); + }); const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); await waitForCondition(checkIfFileHasBeenCreated, timeoutForCellToRun, `"${outputFile}" file not created`); }).timeout(timeoutForCellToRun); @@ -55,26 +60,28 @@ suite('Smoke Test: Interactive Window', () => { EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', - 'pythonFiles', + 'python_files', 'datascience', - 'simple_nb.ipynb' + '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' + 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); + + await vscode.commands.executeCommand('jupyter.opennotebook', vscode.Uri.file(file)); // 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'); + await vscode.commands.executeCommand<void>('jupyter.notebookeditor.runallcells').then(undefined, (err) => { + assert.fail(`Something went wrong running all cells in the native editor: ${err}`); + }); const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); await waitForCondition(checkIfFileHasBeenCreated, timeoutForCellToRun, `"${outputFile}" file not created`); diff --git a/src/test/smoke/jedilsp.smoke.test.ts b/src/test/smoke/jedilsp.smoke.test.ts new file mode 100644 index 000000000000..a2087ff42085 --- /dev/null +++ b/src/test/smoke/jedilsp.smoke.test.ts @@ -0,0 +1,43 @@ +// 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 * as fs from '../../client/common/platform/fs-paths'; +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: Jedi LSP', () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + return undefined; + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Verify diagnostics on a python file', async () => { + const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'python_files', 'intellisense', 'test.py'); + const outputFile = path.join(path.dirname(file), 'ds.log'); + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + const textDocument = await openFile(file); + + waitForCondition( + async () => { + const diagnostics = vscode.languages.getDiagnostics(textDocument.uri); + return diagnostics && diagnostics.length >= 1; + }, + 60_000, + `No diagnostics found in file with invalid syntax`, + ); + }); +}); diff --git a/src/test/smoke/languageServer.smoke.test.ts b/src/test/smoke/languageServer.smoke.test.ts deleted file mode 100644 index 1e21bda47b94..000000000000 --- a/src/test/smoke/languageServer.smoke.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// 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 index 013e6bff396d..4bdec0843862 100644 --- a/src/test/smoke/runInTerminal.smoke.test.ts +++ b/src/test/smoke/runInTerminal.smoke.test.ts @@ -3,11 +3,10 @@ 'use strict'; -// tslint:disable:max-func-body-length no-invalid-this no-any - -import * as fs from 'fs-extra'; +import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; import { openFile, waitForCondition } from '../common'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; @@ -18,32 +17,44 @@ suite('Smoke Test: Run Python File In Terminal', () => { return this.skip(); } await initialize(); + // Ensure the environments extension is not used for this test + await vscode.workspace + .getConfiguration('python') + .update('useEnvironmentsExtension', false, vscode.ConfigurationTarget.Global); + return undefined; }); + setup(initializeTest); suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); - test('Exec', async () => { + // TODO: Re-enable this test once the flakiness on Windows is resolved + test('Exec', async function () { + if (process.platform === 'win32') { + return this.skip(); + } const file = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', - 'testExecInTerminal.py' + 'testExecInTerminal.py', ); const outputFile = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', - 'testExecInTerminal.log' + '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); + await vscode.commands.executeCommand<void>('python.execInTerminal', textDocument.uri).then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); }); diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts new file mode 100644 index 000000000000..cae41cc094d5 --- /dev/null +++ b/src/test/smoke/smartSend.smoke.test.ts @@ -0,0 +1,84 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { assert } from 'chai'; +import * as fs from '../../client/common/platform/fs-paths'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { openFile, waitForCondition } from '../common'; + +suite('Smoke Test: Run Smart Selection and Advance Cursor', async () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + return undefined; + }); + + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + // TODO: Re-enable this test once the flakiness on Windows, linux are resolved + test.skip('Smart Send', async function () { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'create_delete_file.py', + ); + const outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'smart_send_smoke.txt', + ); + + await fs.remove(outputFile); + + const textDocument = await openFile(file); + + if (vscode.window.activeTextEditor) { + const myPos = new vscode.Position(0, 0); + vscode.window.activeTextEditor!.selections = [new vscode.Selection(myPos, myPos)]; + } + await vscode.commands + .executeCommand<void>('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, 20_000, `"${outputFile}" file not created`); + + await vscode.commands + .executeCommand<void>('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + await vscode.commands + .executeCommand<void>('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + async function wait() { + return new Promise<void>((resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + }); + } + + await wait(); + + const deletedFile = !(await fs.pathExists(outputFile)); + if (deletedFile) { + assert.ok(true, `"${outputFile}" file has been deleted`); + } else { + assert.fail(`"${outputFile}" file still exists`); + } + }); +}); diff --git a/src/test/smokeTest.ts b/src/test/smokeTest.ts index 79e44f1dcf3d..a101e961e03d 100644 --- a/src/test/smokeTest.ts +++ b/src/test/smokeTest.ts @@ -3,13 +3,10 @@ '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 fs from '../client/common/platform/fs-paths'; import * as glob from 'glob'; import * as path from 'path'; import { unzip } from './common'; @@ -18,23 +15,23 @@ import { EXTENSION_ROOT_DIR_FOR_TESTS, SMOKE_TEST_EXTENSIONS_DIR } from './const class TestRunner { public async start() { console.log('Start Test Runner'); - await this.enableLanguageServer(true); + await this.enableLanguageServer(); 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 + CODE_EXTENSIONS_PATH: SMOKE_TEST_EXTENSIONS_DIR, }; await this.launchTest(env); } - private async enableLanguageServer(enable: boolean) { + private async enableLanguageServer() { // When running smoke tests, we won't have access to unbundled files. - const settings = `{ "python.languageServer": ${enable ? '"Microsoft"' : '"Jedi"'} }`; + const settings = `{ "python.languageServer": "Jedi" }`; await fs.ensureDir( - path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', '.vscode') + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', '.vscode'), ); await fs.writeFile( path.join( @@ -43,14 +40,14 @@ class TestRunner { 'testMultiRootWkspc', 'smokeTests', '.vscode', - 'settings.json' + 'settings.json', ), - settings + settings, ); } private async launchTest(customEnvVars: Record<string, {}>) { console.log('Launch tests in test runner'); - await new Promise((resolve, reject) => { + await new Promise<void>((resolve, reject) => { const env: Record<string, string> = { TEST_FILES_SUFFIX: 'smoke.test', IS_SMOKE_TEST: 'true', @@ -58,14 +55,14 @@ class TestRunner { EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', - 'smokeTests' + 'smokeTests', ), ...process.env, - ...customEnvVars + ...customEnvVars, }; const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { cwd: EXTENSION_ROOT_DIR_FOR_TESTS, - env + env, }); proc.stdout.pipe(process.stdout); proc.stderr.pipe(process.stderr); @@ -83,7 +80,7 @@ class TestRunner { 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]))) + glob.default('*.vsix', (ex, files) => (ex ? reject(ex) : resolve(files[0]))), ); await unzip(extensionFile, targetDir); } diff --git a/src/test/sourceMapSupport.test.ts b/src/test/sourceMapSupport.test.ts deleted file mode 100644 index db7148b20831..000000000000 --- a/src/test/sourceMapSupport.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -// 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 deleted file mode 100644 index 437a7463269e..000000000000 --- a/src/test/sourceMapSupport.unit.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -// 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 index 4a6dd9f2e287..c3a7968c9c7a 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -1,11 +1,24 @@ -// tslint:disable:no-console - +import { spawnSync } from 'child_process'; +import * as fs from '../client/common/platform/fs-paths'; +import * as os from 'os'; import * as path from 'path'; -import { runTests } from 'vscode-test'; +import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; +import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { initializeLogger } from './testLogger'; +import { getChannel } from './utils/vscode'; +import { TestOptions } from '@vscode/test-electron/out/runTest'; -initializeLogger(); +// If running smoke tests, we don't have access to this. +if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { + const logger = require('./testLogger'); + logger.initializeLogger(); +} +function requiresJupyterExtensionToBeInstalled() { + return process.env.INSTALL_JUPYTER_EXTENSION === 'true'; +} +function requiresPylanceExtensionToBeInstalled() { + return process.env.INSTALL_PYLANCE_EXTENSION === 'true'; +} process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; process.env.VSC_PYTHON_CI_TEST = '1'; @@ -16,24 +29,79 @@ 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'; +/** + * Smoke tests & tests running in VSCode require Jupyter extension to be installed. + */ +async function installJupyterExtension(vscodeExecutablePath: string) { + if (!requiresJupyterExtensionToBeInstalled()) { + console.info('Jupyter Extension not required'); + return; + } + console.info('Installing Jupyter Extension'); + const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath, os.platform()); + + // For now install Jupyter from the marketplace + spawnSync(cliPath, ['--install-extension', JUPYTER_EXTENSION_ID], { + encoding: 'utf-8', + stdio: 'inherit', + }); +} -function start() { +async function installPylanceExtension(vscodeExecutablePath: string) { + if (!requiresPylanceExtensionToBeInstalled()) { + console.info('Pylance Extension not required'); + return; + } + console.info('Installing Pylance Extension'); + const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath, os.platform()); + + // For now install pylance from the marketplace + spawnSync(cliPath, ['--install-extension', PYLANCE_EXTENSION_ID], { + encoding: 'utf-8', + stdio: 'inherit', + }); + + // Make sure to enable it by writing to our workspace path settings + await fs.ensureDir(path.join(workspacePath, '.vscode')); + const settingsPath = path.join(workspacePath, '.vscode', 'settings.json'); + if (await fs.pathExists(settingsPath)) { + let settings = JSON.parse(await fs.readFile(settingsPath, 'utf-8')); + settings = { ...settings, 'python.languageServer': 'Pylance' }; + await fs.writeFile(settingsPath, JSON.stringify(settings)); + } else { + const settings = `{ "python.languageServer": "Pylance" }`; + await fs.writeFile(settingsPath, settings); + } +} + +async function start() { console.log('*'.repeat(100)); console.log('Start Standard tests'); - runTests({ + const channel = getChannel(); + console.log(`Using ${channel} build of VS Code.`); + const vscodeExecutablePath = await downloadAndUnzipVSCode(channel); + const baseLaunchArgs = + requiresJupyterExtensionToBeInstalled() || requiresPylanceExtensionToBeInstalled() + ? [] + : ['--disable-extensions']; + await installJupyterExtension(vscodeExecutablePath); + await installPylanceExtension(vscodeExecutablePath); + console.log('VS Code executable', vscodeExecutablePath); + const launchArgs = baseLaunchArgs + .concat([workspacePath]) + .concat(['--enable-proposed-api']) + .concat(['--timeout', '5000']); + console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); + const options: TestOptions = { 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']), + extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test'), + launchArgs, version: channel, - extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' } - }).catch((ex) => { - console.error('End Standard tests (with errors)', ex); - process.exit(1); - }); + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, + }; + await runTests(options); } -start(); +start().catch((ex) => { + console.error('End Standard tests (with errors)', ex); + process.exit(1); +}); diff --git a/src/test/startPage/startPage.functional.test.tsx b/src/test/startPage/startPage.functional.test.tsx deleted file mode 100644 index 803a43688f81..000000000000 --- a/src/test/startPage/startPage.functional.test.tsx +++ /dev/null @@ -1,116 +0,0 @@ -// 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 deleted file mode 100644 index 5aa19afa0e90..000000000000 --- a/src/test/startPage/startPage.unit.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -// 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, - IWebPanelProvider, - 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<IWebPanelProvider>; - 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<IWebPanelProvider>(); - 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 index 0099f4d4ff5e..a9af3adff9a5 100644 --- a/src/test/startupTelemetry.unit.test.ts +++ b/src/test/startupTelemetry.unit.test.ts @@ -5,52 +5,34 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceConfiguration } from 'vscode'; +import { Uri } from 'vscode'; import { IWorkspaceService } from '../client/common/application/types'; -import { DeprecatePythonPath } from '../client/common/experiments/groups'; -import { IExperimentsManager, IInterpreterPathService } from '../client/common/types'; +import { IExperimentService, 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 experimentsManager: TypeMoq.IMock<IExperimentService>; let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; let workspaceService: TypeMoq.IMock<IWorkspaceService>; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - experimentsManager = TypeMoq.Mock.ofType<IExperimentsManager>(); + experimentsManager = TypeMoq.Mock.ofType<IExperimentService>(); 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(IExperimentService)).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 + interpreterPathService + .setup((i) => i.inspect(resource)) .returns(() => ({ globalValue, workspaceValue, workspaceFolderValue } as any)); const result = hasUserDefinedPythonPath(resource, serviceContainer.object); expect(result).to.equal(false, 'Should be false'); @@ -60,21 +42,10 @@ suite('Startup Telemetry - hasUserDefinedPythonPath()', async () => { }); 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(); + .returns(() => ({ globalValue: 'something else' } as any)); + const result = hasUserDefinedPythonPath(resource, serviceContainer.object); + expect(result).to.equal(true, 'Should be true'); }); }); diff --git a/src/test/telemetry/envFileTelemetry.unit.test.ts b/src/test/telemetry/envFileTelemetry.unit.test.ts index 9a37cf76cc47..99b6e0b38ceb 100644 --- a/src/test/telemetry/envFileTelemetry.unit.test.ts +++ b/src/test/telemetry/envFileTelemetry.unit.test.ts @@ -16,7 +16,7 @@ import { EnvFileTelemetryTests, sendActivationTelemetry, sendFileCreationTelemetry, - sendSettingTelemetry + sendSettingTelemetry, } from '../../client/telemetry/envFileTelemetry'; suite('Env file telemetry', () => { @@ -34,23 +34,22 @@ suite('Env file telemetry', () => { const mockWorkspaceConfig = { inspect: () => ({ - defaultValue: defaultEnvFileValue - }) + defaultValue: defaultEnvFileValue, + }), }; - // tslint:disable-next-line: no-any when(workspaceService.getConfiguration('python')).thenReturn(mockWorkspaceConfig as any); - const mockSendTelemetryEvent = ( + const mockSendTelemetryEvent = (( eventName: EventName, _: number | undefined, - { hasCustomEnvPath }: { hasCustomEnvPath: boolean } + { hasCustomEnvPath }: { hasCustomEnvPath: boolean }, ) => { telemetryEvent = { eventName, - hasCustomEnvPath + hasCustomEnvPath, }; - }; + }) as typeof Telemetry.sendTelemetryEvent; sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); }); 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 deleted file mode 100644 index 8c702ea11151..000000000000 --- a/src/test/telemetry/importTracker.unit.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -// 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 index 13c8487fb5b7..d8a6b72eedc6 100644 --- a/src/test/telemetry/index.unit.test.ts +++ b/src/test/telemetry/index.unit.test.ts @@ -2,55 +2,52 @@ // 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 * as sinon from 'sinon'; +import * as fs from '../../client/common/platform/fs-paths'; -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 + 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; + let readJSONSyncStub: sinon.SinonStub; class Reporter { public static eventName: string[] = []; public static properties: Record<string, string>[] = []; public static measures: {}[] = []; - public static errorProps: string[] | undefined; + public static exception: Error | 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[]) { + public sendTelemetryErrorEvent(eventName: string, properties?: {}, measures?: {}) { this.sendTelemetryEvent(eventName, properties, measures); - Reporter.errorProps = errorProps; + } + public sendTelemetryException(_error: Error, _properties?: {}, _measures?: {}): void { + throw new Error('sendTelemetryException is unsupported'); } } setup(() => { - workspaceService = mock(WorkspaceService); process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); clearTelemetryReporter(); Reporter.clear(); }); @@ -59,48 +56,17 @@ suite('Telemetry', () => { 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(); - }); - }); + sinon.restore(); }); test('Send Telemetry', () => { rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + 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]); @@ -109,7 +75,7 @@ suite('Telemetry', () => { }); test('Send Telemetry with no properties', () => { rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); const eventName = 'Testing'; @@ -121,7 +87,7 @@ suite('Telemetry', () => { }); test('Send Telemetry with shared properties', () => { rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); const eventName = 'Testing'; const properties = { hello: 'world', foo: 'bar' }; @@ -130,7 +96,6 @@ suite('Telemetry', () => { 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]); @@ -139,7 +104,7 @@ suite('Telemetry', () => { }); test('Shared properties will replace existing ones', () => { rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); const eventName = 'Testing'; const properties = { hello: 'world', foo: 'bar' }; @@ -148,102 +113,31 @@ suite('Telemetry', () => { 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', () => { + test('Send Exception Telemetry', () => { rewiremock.enable(); const error = new Error('Boo'); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + 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 expectedProperties = { + ...properties, + errorName: error.name, + errorStack: error.stack, }; - const stackTrace = Reporter.properties[0].stackTrace; - delete Reporter.properties[0].stackTrace; - - expect(Reporter.eventName).to.deep.equal(['ERROR']); + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.properties).to.deep.equal([expectedProperties]); 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 index 3b82fed815bd..4c5294a82f49 100644 --- a/src/test/terminals/activation.unit.test.ts +++ b/src/test/terminals/activation.unit.test.ts @@ -2,22 +2,20 @@ // 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 * as sinon from 'sinon'; +import { EventEmitter, 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 { IActiveResourceService, ITerminalManager } from '../../client/common/application/types'; 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 { TerminalAutoActivation } from '../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../client/terminals/types'; import { noop } from '../core'; +import * as extapi from '../../client/envExt/api.internal'; -// tslint:disable-next-line: max-func-body-length suite('Terminal', () => { - suite('Terminal - Terminal Activation', () => { + suite('Terminal Auto Activation', () => { let autoActivation: ITerminalAutoActivation; let manager: ITerminalManager; let activator: ITerminalActivator; @@ -25,8 +23,12 @@ suite('Terminal', () => { let onDidOpenTerminalEventEmitter: EventEmitter<Terminal>; let terminal: Terminal; let nonActivatedTerminal: Terminal; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; setup(() => { + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + manager = mock(TerminalManager); activator = mock(TerminalActivator); resourceService = mock(ActiveResourceService); @@ -38,10 +40,10 @@ suite('Terminal', () => { instance(manager), [], instance(activator), - instance(resourceService) + instance(resourceService), ); - terminal = { + terminal = ({ dispose: noop, hide: noop, name: 'Some Name', @@ -49,9 +51,9 @@ suite('Terminal', () => { processId: Promise.resolve(0), sendText: noop, show: noop, - exitStatus: { code: 0 } - }; - nonActivatedTerminal = { + exitStatus: { code: 0 }, + } as unknown) as Terminal; + nonActivatedTerminal = ({ dispose: noop, hide: noop, creationOptions: { hideFromUser: true }, @@ -59,91 +61,37 @@ suite('Terminal', () => { processId: Promise.resolve(0), sendText: noop, show: noop, - exitStatus: { code: 0 } - }; + exitStatus: { code: 0 }, + } as unknown) as Terminal; autoActivation.register(); }); // teardown(() => fakeTimer.uninstall()); + teardown(() => { + sinon.restore(); + }); test('Should activate terminal', async () => { // Trigger opening a terminal. - // tslint:disable-next-line: no-any - await ((onDidOpenTerminalEventEmitter.fire(terminal) as any) as Promise<void>); + + await ((onDidOpenTerminalEventEmitter.fire(terminal) as unknown) 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>); + + await ((onDidOpenTerminalEventEmitter.fire(nonActivatedTerminal) as unknown) 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); - }); + test('Should not activate terminal when envs extension should handle activation', async () => { + shouldEnvExtHandleActivationStub.returns(true); - teardown(() => { - extensionsChangeEvent.dispose(); - }); + await ((onDidOpenTerminalEventEmitter.fire(terminal) as unknown) as Promise<void>); - 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(); + verify(activator.activateEnvironmentInTerminal(anything(), anything())).never(); }); }); }); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 1765fa1e8d1b..726b118ce180 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -2,17 +2,20 @@ // Licensed under the MIT License. import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; 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'; +import { IConfigurationService } from '../../../client/common/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as extapi from '../../../client/envExt/api.internal'; -// 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>; @@ -20,38 +23,46 @@ suite('Terminal - Code Execution Manager', () => { let disposables: Disposable[] = []; let serviceContainer: TypeMoq.IMock<IServiceContainer>; let documentManager: TypeMoq.IMock<IDocumentManager>; - let shiftEnterBanner: TypeMoq.IMock<IPythonExtensionBanner>; - let fileSystem: TypeMoq.IMock<IFileSystem>; + let configService: TypeMoq.IMock<IConfigurationService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - fileSystem.setup((f) => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + 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 + dispose: () => void 0, }; }); documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); commandManager = TypeMoq.Mock.ofType<ICommandManager>(undefined, TypeMoq.MockBehavior.Strict); serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); executionManager = new CodeExecutionManager( commandManager.object, documentManager.object, disposables, - fileSystem.object, - shiftEnterBanner.object, - serviceContainer.object + configService.object, + serviceContainer.object, ); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { + sinon.restore(); disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); @@ -75,12 +86,15 @@ suite('Terminal - Code Execution Manager', () => { 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 - ]); + expect(sorted).to.deep.equal( + [ + Commands.Exec_In_Separate_Terminal, + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ].sort(), + ); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -133,7 +147,10 @@ suite('Terminal - Code Execution Manager', () => { 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()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -162,7 +179,10 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { @@ -280,7 +300,7 @@ suite('Terminal - Code Execution Manager', () => { await commandHandler!(); executionService.verify( async (e) => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), - TypeMoq.Times.once() + TypeMoq.Times.once(), ); helper.verifyAll(); } diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts index 47f4a4a1f6e4..749d94672765 100644 --- a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts @@ -1,13 +1,17 @@ // 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 * as sinon from 'sinon'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { + IApplicationShell, + 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'; @@ -17,8 +21,12 @@ import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../.. import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; import { ICodeExecutionService } from '../../../client/terminals/types'; import { PYTHON_PATH } from '../../common'; +import { Conda, CONDA_RUN_VERSION } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { SemVer } from 'semver'; +import assert from 'assert'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -// tslint:disable-next-line:max-func-body-length suite('Terminal - Django Shell Code Execution', () => { let executor: ICodeExecutionService; let terminalSettings: TypeMoq.IMock<ITerminalSettings>; @@ -27,23 +35,27 @@ suite('Terminal - Django Shell Code Execution', () => { let platform: TypeMoq.IMock<IPlatformService>; let fileSystem: TypeMoq.IMock<IFileSystem>; let settings: TypeMoq.IMock<IPythonSettings>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let applicationShell: TypeMoq.IMock<IApplicationShell>; 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>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); workspace .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => { return { - dispose: () => void 0 + dispose: () => void 0, }; }); platform = TypeMoq.Mock.ofType<IPlatformService>(); const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); const commandManager = TypeMoq.Mock.ofType<ICommandManager>(); fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); @@ -55,7 +67,9 @@ suite('Terminal - Django Shell Code Execution', () => { platform.object, commandManager.object, fileSystem.object, - disposables + disposables, + interpreterService.object, + applicationShell.object, ); terminalFactory.setup((f) => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); @@ -72,6 +86,7 @@ suite('Terminal - Django Shell Code Execution', () => { }); disposables = []; + sinon.restore(); }); async function testReplCommandArguments( @@ -80,10 +95,12 @@ suite('Terminal - Django Shell Code Execution', () => { expectedPythonPath: string, terminalArgs: string[], expectedTerminalArgs: string[], - resource?: Uri + resource?: Uri, ) { platform.setup((p) => p.isWindows).returns(() => isWindows); - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); @@ -102,7 +119,7 @@ suite('Terminal - Django Shell Code Execution', () => { pythonPath, 'c:/program files/python/python.exe', terminalArgs, - expectedTerminalArgs + expectedTerminalArgs, ); }); @@ -145,8 +162,8 @@ suite('Terminal - Django Shell Code Execution', () => { 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' + `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt()}`, + 'shell', ); await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); @@ -159,8 +176,8 @@ suite('Terminal - Django Shell Code Execution', () => { 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' + path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt(), + 'shell', ); await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); @@ -174,8 +191,8 @@ suite('Terminal - Django Shell Code Execution', () => { 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' + `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt()}`, + 'shell', ); await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); @@ -189,8 +206,8 @@ suite('Terminal - Django Shell Code Execution', () => { 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' + path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt(), + 'shell', ); await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); @@ -200,28 +217,38 @@ suite('Terminal - Django Shell Code Execution', () => { pythonPath: string, terminalArgs: string[], condaEnv: { name: string; path: string }, - resource?: Uri + resource?: Uri, ) { - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); 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); + sinon.stub(Conda, 'getConda').resolves(new Conda(condaFile)); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); + const env = await createCondaEnv(condaEnv, processService.object, fileSystem.object); + if (!env) { + assert(false, 'Should not be undefined for conda version 4.9.0'); + } const procs = createPythonProcessService(processService.object, env); const condaExecutionService = { getInterpreterInformation: env.getInterpreterInformation, getExecutablePath: env.getExecutablePath, isModuleInstalled: env.isModuleInstalled, + getModuleVersion: env.getModuleVersion, getExecutionInfo: env.getExecutionInfo, execObservable: procs.execObservable, execModuleObservable: procs.execModuleObservable, exec: procs.exec, - execModule: procs.execModule + execModule: procs.execModule, + execForLinter: procs.execForLinter, }; const expectedTerminalArgs = [...terminalArgs, 'manage.py', 'shell']; pythonExecutionFactory - .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(condaExecutionService)); const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 518804b87dd4..b7e0d1617884 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -4,31 +4,42 @@ '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 { Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import * as sinon from 'sinon'; +import * as fs from '../../../client/common/platform/fs-paths'; +import { + IActiveResourceService, + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} 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 { + IProcessService, + IProcessServiceFactory, + ObservableExecutionResult, +} from '../../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { Architecture } 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 { InterpreterType, PythonInterpreter } from '../../../client/pythonEnvironments/info'; +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'; +import { PYTHON_PATH, getPythonSemVer } from '../../common'; +import { ReplType } from '../../../client/repl/types'; -const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'python_files', 'terminalExec'); -// tslint:disable-next-line:max-func-body-length -suite('Terminal - Code Execution Helper', () => { +suite('Terminal - Code Execution Helper', async () => { + let activeResourceService: TypeMoq.IMock<IActiveResourceService>; let documentManager: TypeMoq.IMock<IDocumentManager>; let applicationShell: TypeMoq.IMock<IApplicationShell>; let helper: ICodeExecutionHelper; @@ -36,24 +47,35 @@ suite('Terminal - Code Execution Helper', () => { let editor: TypeMoq.IMock<TextEditor>; let processService: TypeMoq.IMock<IProcessService>; let interpreterService: TypeMoq.IMock<IInterpreterService>; - const workingPython: PythonInterpreter = { + let commandManager: TypeMoq.IMock<ICommandManager>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let jsonParseStub: sinon.SinonStub; + const workingPython: PythonEnvironment = { path: PYTHON_PATH, version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python', displayName: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, }; setup(() => { const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); 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 + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + const resource = Uri.parse('a'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any processService.setup((x: any) => x.then).returns(() => undefined); interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) @@ -65,6 +87,9 @@ suite('Terminal - Code Execution Helper', () => { envVariablesProvider .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) .returns(() => Promise.resolve({})); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) .returns(() => processServiceFactory.object); @@ -77,9 +102,34 @@ suite('Terminal - Code Execution Helper', () => { serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) .returns(() => applicationShell.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) .returns(() => envVariablesProvider.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + pythonSettings + .setup((s) => s.REPL) + .returns(() => ({ + enableREPLSmartSend: false, + REPLSmartSend: false, + sendToNativeREPL: false, + })); + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); helper = new CodeExecutionHelper(serviceContainer.object); document = TypeMoq.Mock.ofType<TextDocument>(); @@ -87,113 +137,126 @@ suite('Terminal - Code Execution Helper', () => { editor.setup((e) => e.document).returns(() => document.object); }); - async function ensureBlankLinesAreRemoved(source: string, expectedSource: string) { - const actualProcessService = new ProcessService(new BufferDecoder()); + test('normalizeLines with BASIC_REPL does not attach bracketed paste mode', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const actualProcessService = new ProcessService(); 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); + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => + actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), + ); + + jsonParseStub = sinon.stub(JSON, 'parse'); + const mockResult = { + normalized: 'print("Looks like you are on 3.13")', + attach_bracket_paste: true, + }; + jsonParseStub.returns(mockResult); + + const result = await helper.normalizeLines('print("Looks like you are on 3.13")', ReplType.terminal); + + expect(result).to.equal(`print("Looks like you are on 3.13")`); + jsonParseStub.restore(); }); - 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()); + + test('normalizeLines should not attach bracketed paste for < 3.13', async () => { + jsonParseStub = sinon.stub(JSON, 'parse'); + const mockResult = { + normalized: 'print("Looks like you are not on 3.13")', + attach_bracket_paste: false, + }; + jsonParseStub.returns(mockResult); + + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const actualProcessService = new ProcessService(); 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]); + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => + actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), + ); + + const result = await helper.normalizeLines('print("Looks like you are not on 3.13")', ReplType.terminal); + + expect(result).to.equal('print("Looks like you are not on 3.13")'); + jsonParseStub.restore(); + }); + + test('normalizeLines should call normalizeSelection.py', async () => { + jsonParseStub.restore(); + let execArgs = ''; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_, args: string[]) => { + execArgs = args.join(' '); + return ({} as unknown) as ObservableExecutionResult<string>; }); - 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.'); + + await helper.normalizeLines('print("hello")', ReplType.terminal); + + expect(execArgs).to.contain('normalizeSelection.py'); }); - ['', '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' + + async function ensureCodeIsNormalized(source: string, expectedSource: string) { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const actualProcessService = new ProcessService(); + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => + actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), ); - await ensureBlankLinesAreRemoved(code + EOL + EOL + EOL + EOL, expectedCode + EOL); + const normalizedCode = await helper.normalizeLines(source, ReplType.terminal); + const normalizedExpected = expectedSource.replace(/\r\n/g, '\n'); + expect(normalizedCode).to.be.equal(normalizedExpected); + } + + const pythonTestVersion = await getPythonSemVer(); + if (pythonTestVersion && pythonTestVersion.minor < 13) { + ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach((fileNameSuffix) => { + test(`Ensure code is normalized (Sample${fileNameSuffix}) - Python < 3.13`, async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + 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_selection.py`), + 'utf8', + ); + await ensureCodeIsNormalized(code, expectedCode); + }); }); - }); + } + test("Display message if there's no active file", async () => { documentManager.setup((doc) => doc.activeTextEditor).returns(() => undefined); @@ -271,8 +334,8 @@ suite('Terminal - Code Execution Helper', () => { document.verify((doc) => doc.save(), TypeMoq.Times.never()); }); - test('Returns current line if nothing is selected', async () => { - const lineContents = 'Line Contents'; + test('Selection is empty, return current line', 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); @@ -282,17 +345,137 @@ suite('Terminal - Code Execution Helper', () => { 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)); + test('Single line: text selection without whitespace ', async () => { + // This test verifies following case: + // 1: if (x): + // 2: print(x) + // 3: ↑------↑ <--- selection range + const expected = ' print(x)'; + editor.setup((e) => e.selection).returns(() => new Selection(2, 4, 2, 12)); const textLine = TypeMoq.Mock.ofType<TextLine>(); - textLine.setup((t) => t.text).returns(() => lineContents); + textLine.setup((t) => t.text).returns(() => ' print(x)'); + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'print(x)'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(expected); + }); + + test('Single line: partial text selection without whitespace ', async () => { + // This test verifies following case: + // 1: if (isPrime(x) || isFibonacci(x)): + // 2: ↑--------↑ <--- selection range + const expected = 'isPrime(x)'; + editor.setup((e) => e.selection).returns(() => new Selection(1, 4, 1, 14)); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => 'if (isPrime(x) || isFibonacci(x)):'); + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'isPrime(x)'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(expected); + }); + + test('Multi-line: text selection without whitespace ', async () => { + // This test verifies following case: + // 1: def calc(m, n): + // ↓<------------------------------- selection start + // 2: print(m) + // 3: print(n) + // ↑<------------------------ selection end + const expected = ' print(m)\n print(n)'; + const selection = new Selection(2, 4, 3, 12); + editor.setup((e) => e.selection).returns(() => selection); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType<TextLine>(); + textLine2.setup((t) => t.text).returns(() => ' print(m)'); + const textLine3 = TypeMoq.Mock.ofType<TextLine>(); + textLine3.setup((t) => t.text).returns(() => ' print(n)'); + const textLines = [textLine, textLine2, textLine3]; + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((r: number) => textLines[r - 1].object); + document + .setup((d) => d.getText(new Range(selection.start, selection.end))) + .returns(() => 'print(m)\n print(n)'); + document + .setup((d) => d.getText(new Range(new Position(selection.start.line, 0), selection.end))) + .returns(() => ' print(m)\n print(n)'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(expected); + }); + + test('Multi-line: text selection without whitespace and partial last line ', async () => { + // This test verifies following case: + // 1: def calc(m, n): + // ↓<------------------------------ selection start + // 2: if (m == 0): + // 3: return n + 1 + // ↑<------------------- selection end (notice " + 1" is not selected) + const expected = ' if (m == 0):\n return n'; + const selection = new Selection(2, 4, 3, 16); + editor.setup((e) => e.selection).returns(() => selection); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType<TextLine>(); + textLine2.setup((t) => t.text).returns(() => ' if (m == 0):'); + const textLine3 = TypeMoq.Mock.ofType<TextLine>(); + textLine3.setup((t) => t.text).returns(() => ' return n + 1'); + const textLines = [textLine, textLine2, textLine3]; + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((r: number) => textLines[r - 1].object); + document + .setup((d) => d.getText(new Range(selection.start, selection.end))) + .returns(() => 'if (m == 0):\n return n'); document - .setup((d) => d.getText(TypeMoq.It.isAny())) - .returns((r: Range) => `${r.start.line}.${r.start.character}.${r.end.line}.${r.end.character}`); + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 4), new Position(selection.start.line, 16))), + ) + .returns(() => 'if (m == 0):'); + document + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 0), new Position(selection.end.line, 20))), + ) + .returns(() => ' if (m == 0):\n return n + 1'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(expected); + }); + + test('Multi-line: partial first and last line', async () => { + // This test verifies following case: + // 1: def calc(m, n): + // ↓<------------------------------- selection start + // 2: if (m > 0 + // 3: and n == 0): + // ↑<-------------------- selection end + // 4: pass + const expected = '(m > 0\n and n == 0)'; + const selection = new Selection(2, 7, 3, 19); + editor.setup((e) => e.selection).returns(() => selection); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType<TextLine>(); + textLine2.setup((t) => t.text).returns(() => ' if (m > 0'); + const textLine3 = TypeMoq.Mock.ofType<TextLine>(); + textLine3.setup((t) => t.text).returns(() => ' and n == 0)'); + const textLines = [textLine, textLine2, textLine3]; + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((r: number) => textLines[r - 1].object); + document + .setup((d) => d.getText(new Range(selection.start, selection.end))) + .returns(() => '(m > 0\n and n == 0)'); + document + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 7), new Position(selection.start.line, 13))), + ) + .returns(() => '(m > 0'); + document + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 0), new Position(selection.end.line, 19))), + ) + .returns(() => ' if (m > 0\n and n == 0)'); const content = await helper.getSelectedTextToExecute(editor.object); - expect(content).to.be.equal('3.0.10.5'); + expect(content).to.be.equal(expected); }); test('saveFileIfDirty will not fail if file is not opened', async () => { @@ -310,15 +493,17 @@ suite('Terminal - Code Execution Helper', () => { .setup((d) => d.textDocuments) .returns(() => [document.object]) .verifiable(TypeMoq.Times.once()); - document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isUntitled).returns(() => true); 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); + const untitledUri = Uri.file('Untitled-1'); + document.setup((doc) => doc.uri).returns(() => untitledUri); + const expectedSavedUri = Uri.file('one.py'); + workspaceService.setup((w) => w.save(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); - await helper.saveFileIfDirty(expectedUri); - documentManager.verifyAll(); - document.verify((doc) => doc.save(), TypeMoq.Times.once()); + const savedUri = await helper.saveFileIfDirty(untitledUri); + + expect(savedUri?.fsPath).to.be.equal(expectedSavedUri.fsPath); }); test('File will be not saved if file is not dirty', async () => { diff --git a/src/test/terminals/codeExecution/smartSend.test.ts b/src/test/terminals/codeExecution/smartSend.test.ts new file mode 100644 index 000000000000..99ccd5d51d80 --- /dev/null +++ b/src/test/terminals/codeExecution/smartSend.test.ts @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as TypeMoq from 'typemoq'; +import * as path from 'path'; +import { TextEditor, Selection, Position, TextDocument, Uri } from 'vscode'; +import { SemVer } from 'semver'; +import { assert, expect } from 'chai'; +import * as fs from '../../../client/common/platform/fs-paths'; +import { + IActiveResourceService, + IApplicationShell, + ICommandManager, + IDocumentManager, +} from '../../../client/common/application/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IConfigurationService, IExperimentService, IPythonSettings } from '../../../client/common/types'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { Commands, EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PYTHON_PATH, getPythonSemVer } from '../../common'; +import { Architecture } from '../../../client/common/utils/platform'; +import { ProcessService } from '../../../client/common/process/proc'; +import { l10n } from '../../mocks/vsc'; +import { ReplType } from '../../../client/repl/types'; + +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'python_files', 'terminalExec'); + +suite('REPL - Smart Send', async () => { + let documentManager: TypeMoq.IMock<IDocumentManager>; + let applicationShell: TypeMoq.IMock<IApplicationShell>; + + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let commandManager: TypeMoq.IMock<ICommandManager>; + + let processServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let codeExecutionHelper: ICodeExecutionHelper; + let experimentService: TypeMoq.IMock<IExperimentService>; + + let processService: TypeMoq.IMock<IProcessService>; + let activeResourceService: TypeMoq.IMock<IActiveResourceService>; + + let document: TypeMoq.IMock<TextDocument>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + + 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, + }; + + // suite set up only run once for each suite. Very start + // set up --- before each test + // tests -- actual tests + // tear down -- run after each test + // suite tear down only run once at the very end. + + // all object that is common to every test. What each test needs + setup(() => { + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); + processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + experimentService = TypeMoq.Mock.ofType<IExperimentService>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + const resource = Uri.parse('a'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((x: any) => x.then).returns(() => undefined); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IExperimentService))) + .returns(() => experimentService.object); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + + pythonSettings + .setup((s) => s.REPL) + .returns(() => ({ + enableREPLSmartSend: true, + REPLSmartSend: true, + sendToNativeREPL: false, + })); + + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object); + document = TypeMoq.Mock.ofType<TextDocument>(); + }); + + test('Cursor is not moved when explicit selection is present', async () => { + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', ReplType.terminal, wholeFileContent); + + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.never()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + commandManager.verifyAll(); + }); + + const pythonTestVersion = await getPythonSemVer(); + + if (pythonTestVersion && pythonTestVersion.minor < 13) { + test('Smart send should perform smart selection and move cursor - Python < 3.13', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualSmartOutput = await codeExecutionHelper.normalizeLines( + 'my_dict = {', + ReplType.terminal, + wholeFileContent, + ); + + // my_dict = { <----- smart shift+enter here + // "key1": "value1", + // "key2": "value2" + // } <---- cursor should be here afterwards, hence offset 3 + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; + expect(actualSmartOutput).to.be.equal(expectedSmartOutput); + commandManager.verifyAll(); + }); + } + + // Do not perform smart selection when there is explicit selection + test('Smart send should not perform smart selection when there is explicit selection', async () => { + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualNonSmartResult = await codeExecutionHelper.normalizeLines( + 'my_dict = {', + ReplType.terminal, + wholeFileContent, + ); + const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic + expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult); + }); + + test('Smart Send should provide warning when code is not valid', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile( + path.join(TEST_FILES_PATH, `sample_invalid_smart_selection.py`), + 'utf8', + ); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', ReplType.terminal, wholeFileContent); + + applicationShell + .setup((a) => + a.showWarningMessage( + l10n.t( + 'Python is unable to parse the code provided. Please turn off Smart Send if you wish to always run line by line or explicitly select code to force run. [logs](command:{0}) for more details.', + Commands.ViewOutput, + ), + 'Switch to line-by-line', + ), + ) + .verifiable(TypeMoq.Times.once()); + }); +}); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index eb05531b0405..b5bcecd971ea 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -1,25 +1,38 @@ // 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 { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { + IApplicationShell, + 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 { + ITerminalService, + ITerminalServiceFactory, + TerminalCreationOptions, +} from '../../../client/common/terminal/types'; import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; +import { Conda, CONDA_RUN_VERSION } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; 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'; +import * as sinon from 'sinon'; +import { assert } from 'chai'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; suite('Terminal - Code Execution', () => { ['Terminal Execution', 'Repl Execution', 'Django Execution'].forEach((testSuiteName) => { @@ -37,7 +50,9 @@ suite('Terminal - Code Execution', () => { let commandManager: TypeMoq.IMock<ICommandManager>; let fileSystem: TypeMoq.IMock<IFileSystem>; let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; let isDjangoRepl: boolean; + let applicationShell: TypeMoq.IMock<IApplicationShell>; teardown(() => { disposables.forEach((disposable) => { @@ -45,7 +60,7 @@ suite('Terminal - Code Execution', () => { disposable.dispose(); } }); - + sinon.restore(); disposables = []; }); @@ -61,6 +76,8 @@ suite('Terminal - Code Execution', () => { commandManager = TypeMoq.Mock.ofType<ICommandManager>(); fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); settings = TypeMoq.Mock.ofType<IPythonSettings>(); settings.setup((s) => s.terminal).returns(() => terminalSettings.object); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); @@ -72,7 +89,10 @@ suite('Terminal - Code Execution', () => { configService.object, workspace.object, disposables, - platform.object + platform.object, + interpreterService.object, + commandManager.object, + applicationShell.object, ); break; } @@ -82,7 +102,10 @@ suite('Terminal - Code Execution', () => { configService.object, workspace.object, disposables, - platform.object + platform.object, + interpreterService.object, + commandManager.object, + applicationShell.object, ); expectedTerminalTitle = 'REPL'; break; @@ -91,7 +114,7 @@ suite('Terminal - Code Execution', () => { isDjangoRepl = true; workspace .setup((w) => - w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), ) .returns(() => { return { dispose: noop }; @@ -104,7 +127,9 @@ suite('Terminal - Code Execution', () => { platform.object, commandManager.object, fileSystem.object, - disposables + disposables, + interpreterService.object, + applicationShell.object, ); expectedTerminalTitle = 'Django Shell'; break; @@ -118,19 +143,25 @@ suite('Terminal - Code Execution', () => { suite(`${testSuiteName} (validation of title)`, () => { setup(() => { terminalFactory - .setup((f) => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isValue(expectedTerminalTitle))) + .setup((f) => + f.getTerminalService( + TypeMoq.It.is<TerminalCreationOptions>((a) => a.title === expectedTerminalTitle), + ), + ) .returns(() => terminalService.object); }); async function ensureTerminalIsCreatedUponInvokingInitializeRepl( isWindows: boolean, isOsx: boolean, - isLinux: 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); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => []); await executor.initializeRepl(); @@ -150,29 +181,77 @@ suite('Terminal - Code Execution', () => { }); 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())) + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) .returns(() => terminalService.object); }); + async function ensureWeSetCurrentDriveBeforeChangingDirectory(_isWindows: boolean): Promise<void> { + const file = Uri.file(path.join('d:', 'path', 'to', 'file', 'one.py')); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.rootPath).returns(() => path.join('c:', 'path', 'to')); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(path.join('c:', 'path', 'to'))); + platform.setup((p) => p.isWindows).returns(() => true); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue('d:')), TypeMoq.Times.once()); + } + test('Ensure we set current drive before changing directory on windows', async () => { + await ensureWeSetCurrentDriveBeforeChangingDirectory(true); + }); + + test('Ensure once set current drive before, we always send command to change the drive letter for subsequent executions', async () => { + await ensureWeSetCurrentDriveBeforeChangingDirectory(true); + const file = Uri.file(path.join('c:', 'path', 'to', 'file', 'one.py')); + await executor.executeFile(file); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue('c:')), TypeMoq.Times.once()); + }); + + async function ensureWeDoNotChangeDriveIfDriveLetterSameAsFileDriveLetter( + _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.rootPath).returns(() => path.join('c:', 'path', 'to')); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(path.join('c:', 'path', 'to'))); + platform.setup((p) => p.isWindows).returns(() => true); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue('c:')), TypeMoq.Times.never()); + } + test('Ensure we do not change drive if current drive letter is same as the file drive letter on windows', async () => { + await ensureWeDoNotChangeDriveIfDriveLetterSameAsFileDriveLetter(true); + }); + 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); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); 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() + t.sendText( + TypeMoq.It.isValue(`cd ${path.dirname(file.fsPath).fileToCommandArgumentForPythonExt()}`), + ), + TypeMoq.Times.once(), ); } test('Ensure we set current directory before executing file (non windows)', async () => { @@ -188,11 +267,13 @@ suite('Terminal - Code Execution', () => { 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); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => []); await executor.executeFile(file); - const dir = path.dirname(file.fsPath).fileToCommandArgument(); + const dir = path.dirname(file.fsPath).fileToCommandArgumentForPythonExt(); terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue(`cd ${dir}`)), TypeMoq.Times.once()); } @@ -204,8 +285,8 @@ suite('Terminal - Code Execution', () => { await ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(true); }); - async function ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory( - isWindows: boolean + async function ensureWeSetCurrentDirectoryBeforeExecutingFileInWorkspaceDirectory( + 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); @@ -214,28 +295,32 @@ suite('Terminal - Code Execution', () => { .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); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => []); await executor.executeFile(file); - terminalService.verify(async (t) => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.once()); } - 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 set current directory before executing file if in the same directory as the current workspace (non windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingFileInWorkspaceDirectory(false); }); - test('Ensure we do not set current directory before executing file if in the same directory (windows)', async () => { - await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(true); + test('Ensure we set current directory before executing file if in the same directory as the current workspace (windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingFileInWorkspaceDirectory(true); }); async function ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory( - isWindows: boolean + 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); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => []); await executor.executeFile(file); @@ -253,26 +338,26 @@ suite('Terminal - Code Execution', () => { isWindows: boolean, pythonPath: string, terminalArgs: string[], - file: Uri + file: Uri, ): Promise<void> { platform.setup((p) => p.isWindows).returns(() => isWindows); - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); 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()) - ) + .setup((p) => p.createCondaExecutionService(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()); + const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgumentForPythonExt()); terminalService.verify( async (t) => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), - TypeMoq.Times.once() + TypeMoq.Times.once(), ); } @@ -300,40 +385,49 @@ suite('Terminal - Code Execution', () => { pythonPath: string, terminalArgs: string[], file: Uri, - condaEnv: { name: string; path: string } + condaEnv: { name: string; path: string }, ): Promise<void> { - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); 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); + sinon.stub(Conda, 'getConda').resolves(new Conda(condaFile)); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); + const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); + if (!env) { + assert(false, 'Should not be undefined for conda version 4.9.0'); + return; + } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { getInterpreterInformation: env.getInterpreterInformation, getExecutablePath: env.getExecutablePath, isModuleInstalled: env.isModuleInstalled, + getModuleVersion: env.getModuleVersion, getExecutionInfo: env.getExecutionInfo, execObservable: procs.execObservable, execModuleObservable: procs.execModuleObservable, exec: procs.exec, - execModule: procs.execModule + execModule: procs.execModule, + execForLinter: procs.execForLinter, }; pythonExecutionFactory - .setup((p) => - p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(condaExecutionService)); await executor.executeFile(file); - const expectedArgs = [...terminalArgs, file.fsPath.fileToCommandArgument()]; + const expectedArgs = [...terminalArgs, file.fsPath.fileToCommandArgumentForPythonExt()]; terminalService.verify( async (t) => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedArgs)), - TypeMoq.Times.once() + TypeMoq.Times.once(), ); } @@ -341,7 +435,7 @@ suite('Terminal - Code Execution', () => { 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' + path: 'path/to/foo-env', }); }); @@ -349,7 +443,7 @@ suite('Terminal - Code Execution', () => { 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' + path: 'path/to/foo-env', }); }); @@ -357,15 +451,15 @@ suite('Terminal - Code Execution', () => { isWindows: boolean, pythonPath: string, expectedPythonPath: string, - terminalArgs: string[] + terminalArgs: string[], ) { pythonExecutionFactory - .setup((p) => - p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)); platform.setup((p) => p.isWindows).returns(() => isWindows); - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; @@ -413,29 +507,38 @@ suite('Terminal - Code Execution', () => { async function testReplCondaCommandArguments( pythonPath: string, terminalArgs: string[], - condaEnv: { name: string; path: string } + condaEnv: { name: string; path: string }, ) { - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); 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); + sinon.stub(Conda, 'getConda').resolves(new Conda(condaFile)); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); + const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); + if (!env) { + assert(false, 'Should not be undefined for conda version 4.9.0'); + return; + } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { getInterpreterInformation: env.getInterpreterInformation, getExecutablePath: env.getExecutablePath, isModuleInstalled: env.isModuleInstalled, + getModuleVersion: env.getModuleVersion, getExecutionInfo: env.getExecutionInfo, execObservable: procs.execObservable, execModuleObservable: procs.execModuleObservable, exec: procs.exec, - execModule: procs.execModule + execModule: procs.execModule, + execForLinter: procs.execForLinter, }; pythonExecutionFactory - .setup((p) => - p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(condaExecutionService)); const djangoArgs = isDjangoRepl ? ['manage.py', 'shell'] : []; @@ -451,26 +554,26 @@ suite('Terminal - Code Execution', () => { 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' + 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' + 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() + TypeMoq.Times.never(), ); terminalService.verify(async (t) => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); }); @@ -480,7 +583,9 @@ suite('Terminal - Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; platform.setup((p) => p.isWindows).returns(() => false); - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); await executor.execute('cmd1'); @@ -491,54 +596,40 @@ suite('Terminal - Code Execution', () => { terminalService.verify( async (t) => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), - TypeMoq.Times.once() + TypeMoq.Times.once(), ); }); - test('Ensure repl is re-initialized when terminal is closed', async () => { + test('Ensure REPL launches after reducing risk of command being ignored or duplicated', 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); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); 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() + // Now check if sendCommand from the initializeRepl is called atLeastOnce. + // This is due to newly added Promise race and fallback to lower risk of swollen first command. + applicationShell.verify( + async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), ); - 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) + applicationShell.verify( + async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), ); - 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) + applicationShell.verify( + async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), ); }); @@ -546,14 +637,41 @@ suite('Terminal - Code Execution', () => { const pythonPath = 'usr/bin/python1234'; const terminalArgs = ['-a', 'b', 'c']; platform.setup((p) => p.isWindows).returns(() => false); - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); await executor.execute('cmd1'); - terminalService.verify(async (t) => t.sendText('cmd1'), TypeMoq.Times.once()); + terminalService.verify(async (t) => t.executeCommand('cmd1', true), TypeMoq.Times.once()); await executor.execute('cmd2'); - terminalService.verify(async (t) => t.sendText('cmd2'), TypeMoq.Times.once()); + terminalService.verify(async (t) => t.executeCommand('cmd2', true), TypeMoq.Times.once()); + }); + + test('Ensure code is sent to the same terminal for a particular resource', async () => { + const resource = Uri.file('a'); + terminalFactory.reset(); + terminalFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) + .callback((options: TerminalCreationOptions) => { + assert.deepEqual(options.resource, resource); + }) + .returns(() => terminalService.object); + + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup((p) => p.isWindows).returns(() => false); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1', resource); + terminalService.verify(async (t) => t.executeCommand('cmd1', true), TypeMoq.Times.once()); + + await executor.execute('cmd2', resource); + terminalService.verify(async (t) => t.executeCommand('cmd2', true), TypeMoq.Times.once()); }); }); }); diff --git a/src/test/terminals/serviceRegistry.unit.test.ts b/src/test/terminals/serviceRegistry.unit.test.ts index 67446f44e601..4f865cdedc0d 100644 --- a/src/test/terminals/serviceRegistry.unit.test.ts +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -1,22 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as typemoq from 'typemoq'; -import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; import { IServiceManager } from '../../client/ioc/types'; -import { ExtensionActivationForTerminalActivation, TerminalAutoActivation } from '../../client/terminals/activation'; +import { 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 { TerminalDeactivateService } from '../../client/terminals/envCollectionActivation/deactivateService'; +import { TerminalIndicatorPrompt } from '../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { TerminalEnvVarCollectionService } from '../../client/terminals/envCollectionActivation/service'; import { registerTypes } from '../../client/terminals/serviceRegistry'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, - ITerminalAutoActivation + IShellIntegrationDetectionService, + ITerminalAutoActivation, + ITerminalDeactivateService, + ITerminalEnvVarCollectionService, } from '../../client/terminals/types'; +import { ShellIntegrationDetectionService } from '../../client/terminals/envCollectionActivation/shellIntegrationService'; suite('Terminal - Service Registry', () => { test('Ensure all services get registered', () => { @@ -25,35 +33,44 @@ suite('Terminal - Service Registry', () => { [ICodeExecutionHelper, CodeExecutionHelper], [ICodeExecutionManager, CodeExecutionManager], [ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell'], - [IExtensionSingleActivationService, ExtensionActivationForTerminalActivation], [ICodeExecutionService, ReplProvider, 'repl'], [ITerminalAutoActivation, TerminalAutoActivation], - [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'] + [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'], + [ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService], + [IExtensionSingleActivationService, TerminalIndicatorPrompt], + [ITerminalDeactivateService, TerminalDeactivateService], + [IShellIntegrationDetectionService, ShellIntegrationDetectionService], ].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) - ) + typemoq.It.is((v: any) => args[0] === v), + typemoq.It.is((value: any) => 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) - ) + typemoq.It.is((v: any) => args[0] === v), + typemoq.It.is((value: any) => args[1] === value), + + typemoq.It.isValue((args[2] as unknown) as string), + ), ) .verifiable(typemoq.Times.once()); } }); + services + .setup((s) => + s.addBinding( + typemoq.It.is((v: any) => ITerminalEnvVarCollectionService === v), + typemoq.It.is((value: any) => IExtensionActivationService === value), + ), + ) + .verifiable(typemoq.Times.once()); registerTypes(services.object); diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts new file mode 100644 index 000000000000..833a4f29e972 --- /dev/null +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { + GlobalEnvironmentVariableCollection, + Uri, + WorkspaceConfiguration, + Disposable, + CancellationToken, + TerminalLinkContext, + Terminal, + EventEmitter, + workspace, +} from 'vscode'; +import { assert } from 'chai'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { registerPythonStartup } from '../../../client/terminals/pythonStartup'; +import { IExtensionContext } from '../../../client/common/types'; +import * as pythonStartupLinkProvider from '../../../client/terminals/pythonStartupLinkProvider'; +import { CustomTerminalLinkProvider } from '../../../client/terminals/pythonStartupLinkProvider'; +import { Repl } from '../../../client/common/utils/localize'; + +suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { + let getConfigurationStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let editorConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let context: TypeMoq.IMock<IExtensionContext>; + let createDirectoryStub: sinon.SinonStub; + let copyStub: sinon.SinonStub; + let globalEnvironmentVariableCollection: TypeMoq.IMock<GlobalEnvironmentVariableCollection>; + + setup(() => { + context = TypeMoq.Mock.ofType<IExtensionContext>(); + globalEnvironmentVariableCollection = TypeMoq.Mock.ofType<GlobalEnvironmentVariableCollection>(); + context.setup((c) => c.environmentVariableCollection).returns(() => globalEnvironmentVariableCollection.object); + context.setup((c) => c.storageUri).returns(() => Uri.parse('a')); + context.setup((c) => c.subscriptions).returns(() => []); + + globalEnvironmentVariableCollection + .setup((c) => c.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve()); + + globalEnvironmentVariableCollection.setup((c) => c.delete(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + createDirectoryStub = sinon.stub(workspaceApis, 'createDirectory'); + copyStub = sinon.stub(workspaceApis, 'copy'); + + pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + editorConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + getConfigurationStub.callsFake((section: string) => { + if (section === 'python') { + return pythonConfig.object; + } + return editorConfig.object; + }); + + createDirectoryStub.callsFake((_) => Promise.resolve()); + copyStub.callsFake((_, __, ___) => Promise.resolve()); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Verify createDirectory is called when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + sinon.assert.calledOnce(createDirectoryStub); + }); + + test('Verify createDirectory is not called when shell integration is disabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + sinon.assert.notCalled(createDirectoryStub); + }); + + test('Verify copy is called when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + sinon.assert.calledOnce(copyStub); + }); + + test('Verify copy is not called when shell integration is disabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + sinon.assert.notCalled(copyStub); + }); + + test('PYTHONSTARTUP is set when enableShellIntegration setting is true', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHONSTARTUP', TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('environmentCollection should not remove PYTHONSTARTUP when enableShellIntegration setting is true', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.never()); + }); + + test('PYTHONSTARTUP is not set when enableShellIntegration setting is false', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHONSTARTUP', TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never(), + ); + }); + + test('PYTHONSTARTUP is deleted when enableShellIntegration setting is false', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.once()); + }); + + test('PYTHON_BASIC_REPL is set when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + await registerPythonStartup(context.object); + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHON_BASIC_REPL', '1', TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('Ensure registering terminal link calls registerTerminalLinkProvider', async () => { + const registerTerminalLinkProviderStub = sinon.stub( + pythonStartupLinkProvider, + 'registerCustomTerminalLinkProvider', + ); + const disposableArray: Disposable[] = []; + pythonStartupLinkProvider.registerCustomTerminalLinkProvider(disposableArray); + + sinon.assert.calledOnce(registerTerminalLinkProviderStub); + sinon.assert.calledWith(registerTerminalLinkProviderStub, disposableArray); + + registerTerminalLinkProviderStub.restore(); + }); + + test('Verify onDidChangeConfiguration is called when configuration changes', async () => { + const onDidChangeConfigurationSpy = sinon.spy(workspace, 'onDidChangeConfiguration'); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + assert.isTrue(onDidChangeConfigurationSpy.calledOnce); + onDidChangeConfigurationSpy.restore(); + }); + + if (process.platform === 'darwin') { + test('Mac - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Cmd click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter<unknown>().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Cmd click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Cmd click to launch VS Code Native REPL'.length, + 'Match expected length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } + if (process.platform !== 'darwin') { + test('Windows/Linux - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Ctrl click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter<unknown>().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Ctrl click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Ctrl click to launch VS Code Native REPL'.length, + 'Match expected Length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } + + test('Verify provideTerminalLinks returns no links when context.line does not contain expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string without the expected link', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter<unknown>().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isArray(links, 'Expected links to be an array'); + assert.isEmpty(links, 'Expected links to be empty'); + }); +}); diff --git a/src/test/testBootstrap.ts b/src/test/testBootstrap.ts index 06d65d41f8fe..ab902255203b 100644 --- a/src/test/testBootstrap.ts +++ b/src/test/testBootstrap.ts @@ -4,7 +4,7 @@ 'use strict'; import { ChildProcess, spawn, SpawnOptions } from 'child_process'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import { AddressInfo, createServer, Server } from 'net'; import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../client/constants'; @@ -13,8 +13,6 @@ 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. @@ -76,7 +74,7 @@ async function end(exitCode: number) { } async function startSocketServer() { - return new Promise((resolve) => { + return new Promise<void>((resolve) => { server = createServer((socket) => { socket.on('data', (buffer) => { const data = buffer.toString('utf8'); @@ -90,13 +88,16 @@ async function startSocketServer() { }); }); - 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.listen( + { host: '127.0.0.1', port: 0 }, + async (): Promise<void> => { + 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); diff --git a/src/test/testLogger.ts b/src/test/testLogger.ts index 5090e23d1faf..26484ee119c7 100644 --- a/src/test/testLogger.ts +++ b/src/test/testLogger.ts @@ -3,22 +3,17 @@ 'use strict'; +import { initializeFileLogging, logTo } from '../client/logging'; +import { LogLevel } from '../client/logging/types'; + // 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); + initializeFileLogging([]); // Send console.*() to the non-console loggers. monkeypatchConsole(); } @@ -37,9 +32,13 @@ function monkeypatchConsole() { const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; const levels: { [key: string]: LogLevel } = { error: LogLevel.Error, - warn: LogLevel.Warn + warn: LogLevel.Warning, + debug: LogLevel.Debug, + trace: LogLevel.Debug, + info: LogLevel.Info, + log: LogLevel.Info, }; - // tslint:disable-next-line:no-any + const consoleAny: any = console; for (const stream of streams) { // Using symbols guarantee the properties will be unique & prevents @@ -47,13 +46,12 @@ function monkeypatchConsole() { // 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); + logTo(level, args); }; } } diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts index 12788f9c93dd..6187597a46a3 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -1,14 +1,13 @@ // 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 { 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. @@ -20,9 +19,9 @@ if (!tty.getWindowSize) { }; } -let mocha = new Mocha(<any>{ +let mocha = new Mocha.default(<any>{ ui: 'tdd', - colors: true + colors: true, }); export type SetupOptions = Mocha.MochaOptions & { @@ -41,7 +40,7 @@ export function configure(setupOptions: SetupOptions): void { } // Force Mocha to exit. (setupOptions as any).exit = true; - mocha = new Mocha(setupOptions); + mocha = new Mocha.default(setupOptions); } export async function run(): Promise<void> { @@ -49,13 +48,6 @@ export async function run(): Promise<void> { // 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, @@ -67,7 +59,7 @@ export async function run(): Promise<void> { */ function initializationScript() { const ex = new Error('Failed to initialize Python extension for tests after 3 minutes'); - let timer: NodeJS.Timer | undefined; + let timer: NodeJS.Timeout | undefined; const failed = new Promise((_, reject) => { timer = setTimeout(() => reject(ex), MAX_EXTENSION_ACTIVATION_TIME); }); @@ -77,7 +69,7 @@ export async function run(): Promise<void> { } // Run the tests. await new Promise<void>((resolve, reject) => { - glob( + glob.default( `**/**.${testFilesGlob}.js`, { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'], cwd: testsRoot }, (error, files) => { @@ -89,14 +81,14 @@ export async function run(): Promise<void> { initializationScript() .then(() => mocha.run((failures) => - failures > 0 ? reject(new Error(`${failures} total failures`)) : resolve() - ) + 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 deleted file mode 100644 index 48083a7fae00..000000000000 --- a/src/test/testing/argsService.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length - -import { fail } 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); - - if (optionsNotFound.length > 0) { - fail('', optionsNotFound.join(', '), 'Options not found'); - } - }); - test('Check for new/unrecognized options without values', () => { - const options = argumentsService.getKnownOptions(); - const optionsNotFound = expectedWithoutArgs.filter((item) => options.withoutArgs.indexOf(item) === -1); - - if (optionsNotFound.length > 0) { - fail('', optionsNotFound.join(', '), 'Options not found'); - } - }); - 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', '-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 deleted file mode 100644 index b7e9c56cd7c2..000000000000 --- a/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -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'; - -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>; - - 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'); - }); - 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(); - }); - 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(); - }); - 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(); - }); -}); - -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 deleted file mode 100644 index 861263d2b575..000000000000 --- a/src/test/testing/codeLenses/testFiles.unit.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -// 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 deleted file mode 100644 index 32061dad8a31..000000000000 --- a/src/test/testing/common/argsHelper.unit.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -// 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 index c1782b7c8027..86e862103bf6 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -3,48 +3,58 @@ '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 sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import * as fs from '../../../client/common/platform/fs-paths'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { CancellationTokenSource, DebugConfiguration, DebugSession, Uri, WorkspaceFolder } from 'vscode'; import { IInvalidPythonPathInDebuggerService } from '../../../client/application/diagnostics/types'; -import { - IApplicationShell, - IDebugService, - IDocumentManager, - IWorkspaceService -} from '../../../client/common/application/types'; +import { IApplicationShell, IDebugService } 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 { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { PythonDebuggerTypeName } 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 { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { DebugLauncher } from '../../../client/testing/common/debugLauncher'; -import { LaunchOptions, TestProvider } from '../../../client/testing/common/types'; +import { LaunchOptions } from '../../../client/testing/common/types'; +import { ITestingSettings } from '../../../client/testing/configuration/types'; +import { TestProvider } from '../../../client/testing/types'; import { isOs, OSType } from '../../common'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import * as envExtApi from '../../../client/envExt/api.internal'; -use(chaiAsPromised); +use(chaiAsPromised.default); -// 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; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let environmentActivationService: TypeMoq.IMock<IEnvironmentActivationService>; + let getWorkspaceFolderStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + const envVars = { FOO: 'BAR' }; + setup(async () => { + environmentActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>(); + environmentActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(envVars)); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); const configService = TypeMoq.Mock.ofType<IConfigurationService>(undefined, TypeMoq.MockBehavior.Strict); serviceContainer @@ -53,21 +63,10 @@ suite('Unit Tests - Debug Launcher', () => { 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); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); const appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict); appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); @@ -76,7 +75,7 @@ suite('Unit Tests - Debug Launcher', () => { 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); + unitTestSettings = TypeMoq.Mock.ofType<ITestingSettings>(); settings.setup((p) => p.testing).returns(() => unitTestSettings.object); debugEnvHelper = TypeMoq.Mock.ofType<IDebugEnvironmentVariablesService>(undefined, TypeMoq.MockBehavior.Strict); @@ -86,72 +85,115 @@ suite('Unit Tests - Debug Launcher', () => { debugLauncher = new DebugLauncher(serviceContainer.object, getNewResolver(configService.object)); }); + + teardown(() => { + sinon.restore(); + }); + function getNewResolver(configService: IConfigurationService) { const validator = TypeMoq.Mock.ofType<IInvalidPythonPathInDebuggerService>( undefined, - TypeMoq.MockBehavior.Strict + 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 + debugEnvHelper.object, + interpreterService.object, + environmentActivationService.object, ); } function setupDebugManager( - workspaceFolder: WorkspaceFolder, + _workspaceFolder: WorkspaceFolder, expected: DebugConfiguration, - testProvider: TestProvider + testProvider: TestProvider, ) { - platformService.setup((p) => p.isWindows).returns(() => /^win/.test(process.platform)); - settings.setup((p) => p.pythonPath).returns(() => 'python'); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); 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())) + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(expected.env)); - //debugService.setup(d => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) + const deferred = createDeferred<void>(); + let capturedConfig: DebugConfiguration | undefined; + + // Use TypeMoq.It.isAny() because the implementation adds a session marker to the config debugService - .setup((d) => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) - .returns((_wspc: WorkspaceFolder, _expectedParam: DebugConfiguration) => { - return Promise.resolve(undefined as any); + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_wspc: WorkspaceFolder, config: DebugConfiguration) => { + capturedConfig = config; + deferred.resolve(); }) - .verifiable(TypeMoq.Times.once()); + .returns(() => Promise.resolve(true)); + + // Setup onDidStartDebugSession - the new implementation uses this to capture the session + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .returns((callback) => { + deferred.promise.then(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }); + return { dispose: () => {} }; + }); + + // Setup onDidTerminateDebugSession - fires after the session starts + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .returns((callback) => { + deferred.promise.then(() => { + setTimeout(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }, 10); + }); + return { dispose: () => {} }; + }); } function createWorkspaceFolder(folderPath: string): WorkspaceFolder { return { index: 0, name: path.basename(folderPath), - uri: Uri.file(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 getTestLauncherScript(testProvider: TestProvider, pythonTestAdapterRewriteExperiment?: boolean) { + if (!pythonTestAdapterRewriteExperiment) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'execution.py'); + } + case 'pytest': { + return path.join(EXTENSION_ROOT_DIR, 'python_files', 'vscode_pytest', 'run_pytest_script.py'); + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } } } } + function getDefaultDebugConfig(): DebugConfiguration { return { name: 'Debug Unit Test', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'launch', console: 'internalConsole', env: {}, @@ -160,65 +202,67 @@ suite('Unit Tests - Debug Launcher', () => { showReturnValue: true, redirectOutput: true, debugStdLib: false, - subProcess: true + subProcess: true, + purpose: [], }; } function setupSuccess( options: LaunchOptions, testProvider: TestProvider, expected?: DebugConfiguration, - debugConfigs?: string | DebugConfiguration[] + debugConfigs?: string | DebugConfiguration[], ) { - const testLaunchScript = getTestLauncherScript(testProvider); + const testLaunchScript = getTestLauncherScript(testProvider, false); 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]); + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); if (!debugConfigs) { - filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + pathExistsStub.resolves(false); } else { - filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + pathExistsStub.resolves(true); + if (typeof debugConfigs !== 'string') { debugConfigs = JSON.stringify({ version: '0.1.0', - configurations: debugConfigs + configurations: debugConfigs, }); } - filesystem - .setup((fs) => fs.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(debugConfigs as string)); + readFileStub.resolves(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]; - } + expected.rules = [{ path: path.join(EXTENSION_ROOT_DIR, 'python_files'), include: false }]; + expected.program = testLaunchScript; + expected.args = options.args; if (!expected.cwd) { expected.cwd = workspaceFolders[0].uri.fsPath; } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + expected.env.TEST_RUN_PIPE = 'pytestPort'; + expected.env.RUN_TEST_IDS_PIPE = 'runTestIdsPort'; // added by LaunchConfigurationResolver: - if (!expected.pythonPath) { - expected.pythonPath = 'python'; + if (!expected.python) { + expected.python = 'python'; } - expected.workspaceFolder = workspaceFolders[0].uri.fsPath; - expected.debugOptions = []; - if (expected.justMyCode === undefined) { - // Populate justMyCode using debugStdLib - expected.justMyCode = !expected.debugStdLib; + if (!expected.clientOS) { + expected.clientOS = isOs(OSType.Windows) ? 'windows' : 'unix'; } - if (!expected.justMyCode) { - expected.debugOptions.push(DebugOptions.DebugStdLib); + if (!expected.debugAdapterPython) { + expected.debugAdapterPython = 'python'; } + if (!expected.debugLauncherPython) { + expected.debugLauncherPython = 'python'; + } + expected.workspaceFolder = workspaceFolders[0].uri.fsPath; + expected.debugOptions = []; if (expected.stopOnEntry) { expected.debugOptions.push(DebugOptions.StopOnEntry); } @@ -238,8 +282,8 @@ suite('Unit Tests - Debug Launcher', () => { setupDebugManager(workspaceFolders[0], expected, testProvider); } - const testProviders: TestProvider[] = ['nosetest', 'pytest', 'unittest']; - // tslint:disable-next-line:max-func-body-length + const testProviders: TestProvider[] = ['pytest', 'unittest']; + testProviders.forEach((testProvider) => { const testTitleSuffix = `(Test Framework '${testProvider}')`; @@ -247,19 +291,27 @@ suite('Unit Tests - Debug Launcher', () => { const options = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; setupSuccess(options, testProvider); await debugLauncher.launchDebugger(options); - debugService.verifyAll(); + try { + debugService.verifyAll(); + } catch (ex) { + console.log(ex); + } }); test(`Must launch debugger with arguments ${testTitleSuffix}`, async () => { const options = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py', '--debug', '1'], - testProvider + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; setupSuccess(options, testProvider); @@ -269,7 +321,7 @@ suite('Unit Tests - Debug Launcher', () => { }); test(`Must not launch debugger if cancelled ${testTitleSuffix}`, async () => { debugService - .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => { return Promise.resolve(undefined as any); }) @@ -278,20 +330,36 @@ suite('Unit Tests - Debug Launcher', () => { const cancellationToken = new CancellationTokenSource(); cancellationToken.cancel(); const token = cancellationToken.token; - const options: LaunchOptions = { cwd: '', args: [], token, testProvider }; + const options: LaunchOptions = { + cwd: '', + args: [], + token, + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; 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; + getWorkspaceFoldersStub.returns(undefined); debugService .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) + .returns(() => { + console.log('Debugging should not start'); + return Promise.resolve(undefined as any); + }) .verifiable(TypeMoq.Times.never()); - const options: LaunchOptions = { cwd: '', args: [], testProvider }; + const options: LaunchOptions = { + cwd: '', + args: [], + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; await expect(debugLauncher.launchDebugger(options)).to.eventually.rejectedWith('Please open a workspace'); @@ -303,12 +371,35 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam'; - setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: DebuggerTypeName, request: 'test' }]); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: PythonDebuggerTypeName, request: 'test' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test('Use cwd value in settings if exist', async () => { + unitTestSettings.setup((p) => p.cwd).returns(() => 'path/to/settings/cwd'); + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + expected.cwd = 'path/to/settings/cwd'; + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + + setupSuccess(options, 'unittest', expected); await debugLauncher.launchDebugger(options); debugService.verifyAll(); @@ -318,34 +409,41 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = { name: 'my tests', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'launch', - pythonPath: 'some/dir/bin/py3', + python: 'some/dir/bin/py3', + debugAdapterPython: 'some/dir/bin/py3', + debugLauncherPython: 'some/dir/bin/py3', stopOnEntry: true, showReturnValue: true, console: 'integratedTerminal', cwd: 'some/dir', env: { - SPAM: 'EGGS' + PYTHONPATH: 'one/two/three', + SPAM: 'EGGS', + TEST_RUN_PIPE: 'pytestPort', + RUN_TEST_IDS_PIPE: 'runTestIdsPort', }, envFile: 'some/dir/.env', redirectOutput: false, debugStdLib: true, - justMyCode: false, // added by LaunchConfigurationResolver: internalConsoleOptions: 'neverOpen', - subProcess: true + subProcess: true, + purpose: [], }; setupSuccess(options, 'unittest', expected, [ { name: 'my tests', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'test', - pythonPath: expected.pythonPath, + pythonPath: expected.python, stopOnEntry: expected.stopOnEntry, showReturnValue: expected.showReturnValue, console: expected.console, @@ -354,8 +452,7 @@ suite('Unit Tests - Debug Launcher', () => { envFile: expected.envFile, redirectOutput: expected.redirectOutput, debugStdLib: expected.debugStdLib, - justMyCode: undefined - } + }, ]); await debugLauncher.launchDebugger(options); @@ -367,14 +464,16 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; 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' } + { name: 'spam1', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam2', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam3', type: PythonDebuggerTypeName, request: 'test' }, ]); await debugLauncher.launchDebugger(options); @@ -386,7 +485,9 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, ']'); @@ -401,7 +502,7 @@ suite('Unit Tests - Debug Launcher', () => { '// test 2 \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ', @@ -409,7 +510,7 @@ suite('Unit Tests - Debug Launcher', () => { [ \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ] \n\ @@ -419,12 +520,12 @@ suite('Unit Tests - Debug Launcher', () => { "configurations": [ \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ] \n\ } \n\ - ' + ', ]; for (const text of malformedFiles) { const testID = text.split('\n')[0].substring(3).trim(); @@ -432,7 +533,9 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, text); @@ -447,20 +550,21 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; 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: 'spam2', type: PythonDebuggerTypeName } as DebugConfiguration, { name: 'spam3', request: 'test' } as DebugConfiguration, - { type: DebuggerTypeName } as DebugConfiguration, - { type: DebuggerTypeName, request: 'test' } as DebugConfiguration, - { request: 'test' } as DebugConfiguration + { type: PythonDebuggerTypeName } as DebugConfiguration, + { type: PythonDebuggerTypeName, request: 'test' } as DebugConfiguration, + { request: 'test' } as DebugConfiguration, ]); - // tslint:enable:no-object-literal-type-assertion await debugLauncher.launchDebugger(options); @@ -471,7 +575,9 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, [{ name: 'foo', type: 'other', request: 'bar' }]); @@ -485,10 +591,12 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); - setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: DebuggerTypeName, request: 'bogus' }]); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: PythonDebuggerTypeName, request: 'bogus' }]); await debugLauncher.launchDebugger(options); @@ -499,12 +607,14 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, [ - { name: 'spam', type: DebuggerTypeName, request: 'launch' }, - { name: 'spam', type: DebuggerTypeName, request: 'attach' } + { name: 'spam', type: PythonDebuggerTypeName, request: 'launch' }, + { name: 'spam', type: PythonDebuggerTypeName, request: 'attach' }, ]); await debugLauncher.launchDebugger(options); @@ -516,17 +626,19 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; 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' } + { name: 'spam1', type: PythonDebuggerTypeName, request: 'launch' }, + { name: 'spam2', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam3', type: PythonDebuggerTypeName, request: 'attach' }, + { name: 'xyz', type: 'another', request: 'abc' }, ]); await debugLauncher.launchDebugger(options); @@ -538,7 +650,9 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam'; @@ -555,14 +669,14 @@ suite('Unit Tests - Debug Launcher', () => { { \n\ // "test" debug config \n\ "name": "spam", /* non-empty */ \n\ - "type": "python", /* must be "python" */ \n\ + "type": "debugpy", /* must be "python" */ \n\ "request": "test", /* must be "test" */ \n\ // extra stuff here: \n\ "stopOnEntry": true \n\ } \n\ ] \n\ } \n\ - ' + ', ); await debugLauncher.launchDebugger(options); @@ -573,8 +687,8 @@ suite('Unit Tests - Debug Launcher', () => { 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)); + pathExistsStub.resolves(true); + readFileStub.withArgs(filename).resolves(jsonc); const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); @@ -585,11 +699,235 @@ suite('Unit Tests - Debug Launcher', () => { 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)); + pathExistsStub.resolves(true); + readFileStub.withArgs(filename).resolves(jsonc); const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); expect(configs).to.be.deep.equal([]); }); + + // ===== PROJECT-BASED DEBUG SESSION TESTS ===== + + suite('Project-based debug sessions', () => { + function setupForProjectTests(options: LaunchOptions) { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); + settings.setup((p) => p.envFile).returns(() => __filename); + + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + + const workspaceFolders = [{ index: 0, name: 'test', uri: Uri.file(options.cwd) }]; + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); + pathExistsStub.resolves(false); + + // Stub useEnvExtension to avoid null reference errors in tests + sinon.stub(envExtApi, 'useEnvExtension').returns(false); + } + + /** + * Helper to setup debug service mocks with proper session lifecycle simulation. + * The implementation uses onDidStartDebugSession to capture the session via marker, + * then onDidTerminateDebugSession to resolve when that session ends. + */ + function setupDebugServiceWithSessionLifecycle(): { + capturedConfigs: DebugConfiguration[]; + } { + const capturedConfigs: DebugConfiguration[] = []; + let startCallback: ((session: DebugSession) => void) | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfigs.push(config); + // Simulate the full session lifecycle after startDebugging resolves + setTimeout(() => { + const session = ({ + id: `session-${capturedConfigs.length}`, + configuration: config, + } as unknown) as DebugSession; + // Fire start first (so ourSession is captured) + startCallback?.(session); + // Then fire terminate (so the promise resolves) + setTimeout(() => terminateCallback?.(session), 5); + }, 5); + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + return { capturedConfigs }; + } + + test('should use project name in config name when provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + project: { name: 'myproject (Python 3.11)', uri: Uri.file('one/two/three') }, + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + expect(capturedConfigs[0].name).to.equal('Debug Tests: myproject (Python 3.11)'); + }); + + test('should use default python when no project provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should use the default 'python' from interpreterService mock + expect(capturedConfigs[0].python).to.equal('python'); + }); + + test('should add unique session marker to launch config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should have a session marker of format 'test-{timestamp}-{random}' + const marker = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + expect(marker).to.be.a('string'); + expect(marker).to.match(/^test-\d+-[a-z0-9]+$/); + }); + + test('should generate unique markers for each launch', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + // Launch twice + await debugLauncher.launchDebugger(options); + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(2); + const marker1 = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + const marker2 = (capturedConfigs[1] as any).__vscodeTestSessionMarker; + expect(marker1).to.not.equal(marker2); + }); + + test('should only resolve when matching session terminates', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + + let capturedConfig: DebugConfiguration | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + let startCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfig = config; + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + const launchPromise = debugLauncher.launchDebugger(options); + + // Wait for config to be captured + await new Promise((r) => setTimeout(r, 10)); + + // Simulate our session starting + const ourSession = ({ + id: 'our-session-id', + configuration: capturedConfig!, + } as unknown) as DebugSession; + startCallback?.(ourSession); + + // Create a different session (like another project's debug) + const otherSession = ({ + id: 'other-session-id', + configuration: { __vscodeTestSessionMarker: 'different-marker' }, + } as unknown) as DebugSession; + + // Terminate the OTHER session first - should NOT resolve our promise + terminateCallback?.(otherSession); + + // Wait a bit to ensure it didn't resolve + let resolved = false; + const checkPromise = launchPromise.then(() => { + resolved = true; + }); + + await new Promise((r) => setTimeout(r, 20)); + expect(resolved).to.be.false; + + // Now terminate OUR session - should resolve + terminateCallback?.(ourSession); + + await checkPromise; + expect(resolved).to.be.true; + }); + }); }); diff --git a/src/test/testing/common/helpers.unit.test.ts b/src/test/testing/common/helpers.unit.test.ts new file mode 100644 index 000000000000..441b257d4d0e --- /dev/null +++ b/src/test/testing/common/helpers.unit.test.ts @@ -0,0 +1,48 @@ +import * as path from 'path'; +import * as assert from 'assert'; +import { addPathToPythonpath } from '../../../client/testing/common/helpers'; + +suite('Unit Tests - Test Helpers', () => { + const newPaths = [path.join('path', 'to', 'new')]; + test('addPathToPythonpath handles undefined path', async () => { + const launchPythonPath = undefined; + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + assert.equal(actualPath, path.join('path', 'to', 'new')); + }); + test('addPathToPythonpath adds path if it does not exist in the python path', async () => { + const launchPythonPath = path.join('random', 'existing', 'pythonpath'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('random', 'existing', 'pythonpath') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath does not add to python path if the given python path already contains the path', async () => { + const launchPythonPath = path.join('path', 'to', 'new'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath correctly normalizes both existing and new paths', async () => { + const newerPaths = [path.join('path', 'to', '/', 'new')]; + const launchPythonPath = path.join('path', 'to', '..', 'old'); + const actualPath = addPathToPythonpath(newerPaths, launchPythonPath); + const expectedPath = path.join('path', 'old') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath splits pythonpath then rejoins it', async () => { + const launchPythonPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + assert.equal(actualPath, expectedPath); + }); +}); diff --git a/src/test/testing/common/managers/baseTestManager.unit.test.ts b/src/test/testing/common/managers/baseTestManager.unit.test.ts deleted file mode 100644 index 47adcb9809c8..000000000000 --- a/src/test/testing/common/managers/baseTestManager.unit.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -// 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 index 7baa68ef07e4..1b049d4f3fbe 100644 --- a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -3,23 +3,25 @@ '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 { IInstaller, ILogOutputChannel, 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'; +import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; +import { TestConfigurationManager } from '../../../../client/testing/common/testConfigurationManager'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; class MockTestConfigurationManager extends TestConfigurationManager { - public requiresUserToConfigure(_wkspace: Uri): Promise<boolean> { + // The workspace arg is ignored. + // eslint-disable-next-line class-methods-use-this + public requiresUserToConfigure(): Promise<boolean> { throw new Error('Method not implemented.'); } - public configure(_wkspace: any): Promise<any> { + + // The workspace arg is ignored. + // eslint-disable-next-line class-methods-use-this + public configure(): Promise<void> { throw new Error('Method not implemented.'); } } @@ -39,7 +41,7 @@ suite('Unit Test Configuration Manager (unit)', () => { 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))) + .setup((s) => s.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel); serviceContainer .setup((s) => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))) @@ -48,7 +50,7 @@ suite('Unit Test Configuration Manager (unit)', () => { manager = new MockTestConfigurationManager( workspaceUri, product as UnitTestProduct, - serviceContainer.object + serviceContainer.object, ); }); diff --git a/src/test/testing/common/services/configSettingService.unit.test.ts b/src/test/testing/common/services/configSettingService.unit.test.ts index 09e947efb25f..d369d7ead825 100644 --- a/src/test/testing/common/services/configSettingService.unit.test.ts +++ b/src/test/testing/common/services/configSettingService.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -14,19 +12,16 @@ 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'; +import { TestConfigSettingsService } from '../../../../client/testing/common/configSettingService'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; +import { BufferedTestConfigSettingsService } from '../../../../client/testing/common/bufferedTestConfigSettingService'; -use(chaiPromise); +use(chaiPromise.default); const updateMethods: (keyof Omit<ITestConfigSettingsService, 'getTestEnablingSetting'>)[] = [ 'updateTestArgs', 'disable', - 'enable' + 'enable', ]; suite('Unit Tests - ConfigSettingsService', () => { @@ -53,8 +48,6 @@ suite('Unit Tests - ConfigSettingsService', () => { return 'testing.unittestArgs'; case Product.pytest: return 'testing.pytestArgs'; - case Product.nosetest: - return 'testing.nosetestArgs'; default: throw new Error('Invalid Test Product'); } @@ -65,8 +58,6 @@ suite('Unit Tests - ConfigSettingsService', () => { return 'testing.unittestEnabled'; case Product.pytest: return 'testing.pytestEnabled'; - case Product.nosetest: - return 'testing.nosetestsEnabled'; default: throw new Error('Invalid Test Product'); } @@ -88,11 +79,6 @@ suite('Unit Tests - ConfigSettingsService', () => { } } 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'))) @@ -115,11 +101,6 @@ suite('Unit Tests - ConfigSettingsService', () => { 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) @@ -133,7 +114,7 @@ suite('Unit Tests - ConfigSettingsService', () => { const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); workspaceService .setup((w) => - w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)) + w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)), ) .returns(() => pythonConfig.object) .verifiable(typeMoq.Times.once()); @@ -154,11 +135,6 @@ suite('Unit Tests - ConfigSettingsService', () => { 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) @@ -176,7 +152,7 @@ suite('Unit Tests - ConfigSettingsService', () => { const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); workspaceService .setup((w) => - w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)) + w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)), ) .returns(() => pythonConfig.object) .verifiable(typeMoq.Times.once()); @@ -197,11 +173,6 @@ suite('Unit Tests - ConfigSettingsService', () => { 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) @@ -236,17 +207,14 @@ suite('Unit Tests - BufferedTestConfigSettingsService', () => { c.updateTestArgs( typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.pytest), - typeMoq.It.isValue(newArgs) - ) + 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()); @@ -254,7 +222,6 @@ suite('Unit Tests - BufferedTestConfigSettingsService', () => { 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); diff --git a/src/test/testing/common/services/contextService.unit.test.ts b/src/test/testing/common/services/contextService.unit.test.ts deleted file mode 100644 index 357a46dcdae1..000000000000 --- a/src/test/testing/common/services/contextService.unit.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// 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 deleted file mode 100644 index 9238e75822db..000000000000 --- a/src/test/testing/common/services/discoveredTestParser.unit.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -// 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 deleted file mode 100644 index e7b54d2ec0cc..000000000000 --- a/src/test/testing/common/services/discovery.unit.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -// 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 deleted file mode 100644 index 62e15894fc5e..000000000000 --- a/src/test/testing/common/services/storageService.unit.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -// 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 deleted file mode 100644 index 880d03982ff2..000000000000 --- a/src/test/testing/common/services/testResultsService.unit.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -// 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 deleted file mode 100644 index f2a689c3d5a2..000000000000 --- a/src/test/testing/common/services/testStatusService.unit.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -// 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 deleted file mode 100644 index 82822609856f..000000000000 --- a/src/test/testing/common/testUtils.unit.test.ts +++ /dev/null @@ -1,698 +0,0 @@ -// 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 deleted file mode 100644 index cb09323e87b2..000000000000 --- a/src/test/testing/common/testVisitors/resultResetVisitor.unit.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -// 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/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts new file mode 100644 index 000000000000..478e9dd85744 --- /dev/null +++ b/src/test/testing/common/testingAdapter.test.ts @@ -0,0 +1,1242 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestController, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as sinon from 'sinon'; +import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { + ITestController, + ITestResultResolver, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { traceError, traceLog } from '../../../client/logging'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { TestProvider } from '../../../client/testing/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import * as pixi from '../../../client/pythonEnvironments/common/environmentManagers/pixi'; + +suite('End to End Tests: test adapters', () => { + let resultResolver: ITestResultResolver; + let pythonExecFactory: IPythonExecutionFactory; + let configService: IConfigurationService; + let serviceContainer: IServiceContainer; + let envVarsService: IEnvironmentVariablesProvider; + let workspaceUri: Uri; + let testController: TestController; + let getPixiStub: sinon.SinonStub; + const unittestProvider: TestProvider = UNITTEST_PROVIDER; + const pytestProvider: TestProvider = PYTEST_PROVIDER; + const rootPathSmallWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'smallWorkspace', + ); + const rootPathLargeWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'largeWorkspace', + ); + const rootPathErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'errorWorkspace', + ); + const rootPathDiscoveryErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'discoveryErrorWorkspace', + ); + const rootPathDiscoverySymlink = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'symlinkWorkspace', + ); + const nestedTarget = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testTestingRootWkspc', 'target workspace'); + const nestedSymlink = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'symlink_parent-folder', + ); + const rootPathCoverageWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'coverageWorkspace', + ); + suiteSetup(async () => { + // create symlink for specific symlink test + const target = rootPathSmallWorkspace; + const dest = rootPathDiscoverySymlink; + try { + fs.symlink(target, dest, 'dir', (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink created successfully for regular symlink end to end tests.'); + } + }); + fs.symlink(nestedTarget, nestedSymlink, 'dir', (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink created successfully for nested symlink end to end tests.'); + } + }); + } catch (err) { + traceError(err); + } + }); + + setup(async () => { + serviceContainer = (await initialize()).serviceContainer; + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + + // create objects that were injected + configService = serviceContainer.get<IConfigurationService>(IConfigurationService); + pythonExecFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); + testController = serviceContainer.get<TestController>(ITestController); + envVarsService = serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider); + + // create objects that were not injected + }); + teardown(() => { + sinon.restore(); + }); + suiteTeardown(async () => { + // remove symlink + const dest = rootPathDiscoverySymlink; + if (fs.existsSync(dest)) { + fs.unlink(dest, (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink removed successfully after tests, rootPathDiscoverySymlink.'); + } + }); + } else { + traceLog('Symlink was not found to remove after tests, exiting successfully, rootPathDiscoverySymlink.'); + } + + if (fs.existsSync(nestedSymlink)) { + fs.unlink(nestedSymlink, (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink removed successfully after tests, nestedSymlink.'); + } + }); + } else { + traceLog('Symlink was not found to remove after tests, exiting successfully, nestedSymlink.'); + } + }); + test('unittest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + // const deferredTillEOT = createTestingDeferred(); + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + + // set workspace to test workspace folder and set up settings + + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('unittest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + + // set settings to work for the given workspace + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter nested symlink', async () => { + if (os.platform() === 'win32') { + console.log('Skipping test for windows'); + return; + } + + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + const workspacePath = path.join(nestedSymlink, 'custom_sub_folder'); + const workspacePathParent = nestedSymlink; + workspaceUri = Uri.parse(workspacePath); + const filePath = path.join(workspacePath, 'test_simple.py'); + const stats = fs.lstatSync(workspacePathParent); + + // confirm that the path is a symbolic link + assert.ok(stats.isSymbolicLink(), 'The PARENT path is not a symbolic link but must be for this test.'); + + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + // 4. Confirm that the cwd returned is the symlink path and the test's path is also using the symlink as the root + if (process.platform === 'win32') { + // covert string to lowercase for windows as the path is case insensitive + traceLog('windows machine detected, converting path to lowercase for comparison'); + const a = actualData.cwd.toLowerCase(); + const b = filePath.toLowerCase(); + const testSimpleActual = (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path.toLowerCase(); + const testSimpleExpected = filePath.toLowerCase(); + assert.strictEqual(a, b, `Expected cwd to be the symlink path actual: ${a} expected: ${b}`); + assert.strictEqual( + testSimpleActual, + testSimpleExpected, + `Expected test path to be the symlink path actual: ${testSimpleActual} expected: ${testSimpleExpected}`, + ); + } else { + assert.strictEqual( + path.join(actualData.cwd), + path.join(workspacePath), + 'Expected cwd to be the symlink path, check for non-windows machines', + ); + assert.strictEqual( + (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path, + filePath, + 'Expected test path to be the symlink path, check for non windows machines', + ); + } + + // 5. Confirm that resolveDiscovery was called once + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter small workspace with symlink', async () => { + if (os.platform() === 'win32') { + console.log('Skipping test for windows'); + return; + } + + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + const testSimpleSymlinkPath = path.join(rootPathDiscoverySymlink, 'test_simple.py'); + workspaceUri = Uri.parse(rootPathDiscoverySymlink); + const stats = fs.lstatSync(rootPathDiscoverySymlink); + + // confirm that the path is a symbolic link + assert.ok(stats.isSymbolicLink(), 'The path is not a symbolic link but must be for this test.'); + + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + // 4. Confirm that the cwd returned is the symlink path and the test's path is also using the symlink as the root + if (process.platform === 'win32') { + // covert string to lowercase for windows as the path is case insensitive + traceLog('windows machine detected, converting path to lowercase for comparison'); + const a = actualData.cwd.toLowerCase(); + const b = rootPathDiscoverySymlink.toLowerCase(); + const testSimpleActual = (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path.toLowerCase(); + const testSimpleExpected = testSimpleSymlinkPath.toLowerCase(); + assert.strictEqual(a, b, `Expected cwd to be the symlink path actual: ${a} expected: ${b}`); + assert.strictEqual( + testSimpleActual, + testSimpleExpected, + `Expected test path to be the symlink path actual: ${testSimpleActual} expected: ${testSimpleExpected}`, + ); + } else { + assert.strictEqual( + path.join(actualData.cwd), + path.join(rootPathDiscoverySymlink), + 'Expected cwd to be the symlink path, check for non-windows machines', + ); + assert.strictEqual( + (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path, + testSimpleSymlinkPath, + 'Expected test path to be the symlink path, check for non windows machines', + ); + } + + // 5. Confirm that resolveDiscovery was called once + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('unittest execution adapter small workspace with correct output', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_simple.SimpleClass.test_simple_unit'], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as unittest output + assert.ok( + collectedOutput.includes('expected printed output, stdout'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('expected printed output, stderr'), + 'The test string does not contain the expected stderr output.', + ); + assert.ok( + collectedOutput.includes('Ran 1 test in'), + 'The test string does not contain the expected unittest output.', + ); + }); + }); + test('unittest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + const validStatuses = ['subtest-success', 'subtest-failure']; + assert.ok( + validStatuses.includes(payload.status), + `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${ + payload.status + }`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_parameterized_subtest.NumbersTest.test_even'], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output + assert.ok( + collectedOutput.includes('test_parameterized_subtest.py'), + 'The test string does not contain the correct test name which should be printed', + ); + assert.ok( + collectedOutput.includes('FAILED (failures=1000)'), + 'The test string does not contain the last of the unittest output', + ); + }); + }); + test('pytest execution adapter small workspace with correct output', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as pytest output + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('Captured log call'), + 'The test string does not contain the expected log section.', + ); + const searchStrings = [ + 'This is a warning message.', + 'This is an error message.', + 'This is a critical message.', + ]; + let searchString: string; + for (searchString of searchStrings) { + const count: number = (collectedOutput.match(new RegExp(searchString, 'g')) || []).length; + assert.strictEqual( + count, + 2, + `The test string does not contain two instances of ${searchString}. Should appear twice from logging output and stack trace`, + ); + } + }); + }); + + test('Unittest execution with coverage, small workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + resultResolver._resolveCoverage = (payload, _token?) => { + assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); + assert.ok(payload.result, 'Expected results to be present'); + const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; + assert.ok(simpleFileCov, 'Expected test_simple.py coverage to be present'); + // since only one test was run, the other test in the same file will have missed coverage lines + assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); + assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathCoverageWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_even.TestNumbers.test_odd'], + TestRunProfileKind.Coverage, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + assert.ok(collectedOutput, 'expect output to be collected'); + }); + }); + test('pytest coverage execution, small workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + resultResolver._resolveCoverage = (payload, _runInstance?) => { + assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); + assert.ok(payload.result, 'Expected results to be present'); + const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; + assert.ok(simpleFileCov, 'Expected test_simple.py coverage to be present'); + // since only one test was run, the other test in the same file will have missed coverage lines + assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); + assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathCoverageWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathCoverageWorkspace}/test_even.py::TestNumbers::test_odd`], + TestRunProfileKind.Coverage, + testRun.object, + pythonExecFactory, + ) + .then(() => { + assert.ok(collectedOutput, 'expect output to be collected'); + }); + }); + test('pytest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // generate list of test_ids + const testIds: string[] = []; + for (let i = 0; i < 2000; i = i + 1) { + const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; + testIds.push(testId); + } + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for large repo + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output from pytest.', + ); + }); + }); + test('unittest discovery adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveDiscovery = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest discovery seg fault error handling', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveDiscovery = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`add one to call count, is now ${callCount}`); + traceLog(`pytest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + assert.ok( + callCount >= 1, + `Expected _resolveDiscovery to be called at least once, call count was instead ${callCount}`, + ); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + callCount = callCount + 1; + try { + if ('status' in data) { + if (data.status === 'error') { + assert.ok(data.error, "Expected errors in 'error' field"); + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); + } + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search( + 'test_seg_fault.py::TestSegmentationFault::test_segfault', + ); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + + test('resolveExecution performance test: validates efficient test result processing', async () => { + // This test validates that resolveExecution processes test results efficiently + // without expensive tree rebuilding or linear searching operations. + // + // The test ensures that processing many test results (like parameterized tests) + // remains fast and doesn't cause performance issues or stack overflow. + + // ================================================================ + // SETUP: Initialize test environment and tracking variables + // ================================================================ + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + + // Performance tracking variables + let totalCallTime = 0; + let callCount = 0; + const callTimes: number[] = []; + let treeRebuildCount = 0; + let totalSearchOperations = 0; + + // Test configuration - Moderate scale to validate efficiency + const numTestFiles = 5; // Multiple test files + const testFunctionsPerFile = 10; // Test functions per file + const totalTestItems = numTestFiles * testFunctionsPerFile; // Total test items in mock tree + const numParameterizedResults = 15; // Number of parameterized test results to process + + // ================================================================ + // MOCK: Set up spies and function wrapping to track performance + // ================================================================ + + // Mock getTestCaseNodes to track expensive tree operations + const originalGetTestCaseNodes = require('../../../client/testing/testController/common/testItemUtilities') + .getTestCaseNodes; + const getTestCaseNodesSpy = sinon.stub().callsFake((item) => { + treeRebuildCount++; + const result = originalGetTestCaseNodes(item); + // Track search operations through tree items + // Safely handle undefined results + if (result && Array.isArray(result)) { + totalSearchOperations += result.length; + } + return result || []; // Return empty array if undefined + }); + + // Replace the real function with our spy + const testItemUtilities = require('../../../client/testing/testController/common/testItemUtilities'); + testItemUtilities.getTestCaseNodes = getTestCaseNodesSpy; + + // Stub isTestItemValid to always return true for performance test + // This prevents expensive tree searches during validation + const testItemIndexStub = sinon.stub((resultResolver as any).testItemIndex, 'isTestItemValid').returns(true); + + // Wrap the _resolveExecution function to measure performance + const original_resolveExecution = resultResolver.resolveExecution.bind(resultResolver); + resultResolver.resolveExecution = (payload, runInstance) => { + const startTime = performance.now(); + callCount++; + + // Call the actual implementation + original_resolveExecution(payload, runInstance); + + const endTime = performance.now(); + const callTime = endTime - startTime; + callTimes.push(callTime); + totalCallTime += callTime; + }; + + // ================================================================ + // SETUP: Create test data that simulates realistic test scenarios + // ================================================================ + + // Create a mock TestController with the methods we need + const mockTestController = { + items: new Map(), + createTestItem: (id: string, label: string, uri?: Uri) => { + const childrenMap = new Map(); + // Add forEach method to children map to simulate TestItemCollection + (childrenMap as any).forEach = function (callback: (item: any) => void) { + Map.prototype.forEach.call(this, callback); + }; + + const mockTestItem = { + id, + label, + uri, + children: childrenMap, + parent: undefined, + canResolveChildren: false, + tags: [{ id: 'python-run' }, { id: 'python-debug' }], + }; + return mockTestItem; + }, + // Add a forEach method to simulate the problematic iteration + forEach: function (callback: (item: any) => void) { + this.items.forEach(callback); + }, + }; // Replace the testController in our resolver + (resultResolver as any).testController = mockTestController; + + // Create test controller with many test items (simulates real workspace) + for (let i = 0; i < numTestFiles; i++) { + const testItem = mockTestController.createTestItem( + `test_file_${i}`, + `Test File ${i}`, + Uri.file(`/test_${i}.py`), + ); + mockTestController.items.set(`test_file_${i}`, testItem); + + // Add child test items to each file + for (let j = 0; j < testFunctionsPerFile; j++) { + const childItem = mockTestController.createTestItem( + `test_${i}_${j}`, + `test_method_${j}`, + Uri.file(`/test_${i}.py`), + ); + testItem.children.set(`test_${i}_${j}`, childItem); + + // Set up the ID mappings that the resolver uses + resultResolver.runIdToTestItem.set(`test_${i}_${j}`, childItem as any); + resultResolver.runIdToVSid.set(`test_${i}_${j}`, `test_${i}_${j}`); + resultResolver.vsIdToRunId.set(`test_${i}_${j}`, `test_${i}_${j}`); + } + } // Create payload with multiple test results (simulates real test execution) + const testResults: Record<string, any> = {}; + for (let i = 0; i < numParameterizedResults; i++) { + // Use test IDs that actually exist in our mock setup (test_0_0 through test_0_9) + testResults[`test_0_${i % testFunctionsPerFile}`] = { + test: `test_method[${i}]`, + outcome: 'success', + message: null, + traceback: null, + subtest: null, + }; + } + + const payload: ExecutionTestPayload = { + cwd: '/test', + status: 'success' as const, + error: '', + result: testResults, + }; + + const mockRunInstance = { + passed: sinon.stub(), + failed: sinon.stub(), + errored: sinon.stub(), + skipped: sinon.stub(), + }; + + // ================================================================ + // EXECUTION: Run the performance test + // ================================================================ + + const overallStartTime = performance.now(); + + // Run the resolveExecution function with test data + await resultResolver.resolveExecution(payload, mockRunInstance as any); + + const overallEndTime = performance.now(); + const totalTime = overallEndTime - overallStartTime; + + // ================================================================ + // CLEANUP: Restore original functions + // ================================================================ + testItemUtilities.getTestCaseNodes = originalGetTestCaseNodes; + testItemIndexStub.restore(); + + // ================================================================ + // ASSERT: Verify efficient performance characteristics + // ================================================================ + console.log(`\n=== PERFORMANCE RESULTS ===`); + console.log( + `Test setup: ${numTestFiles} files × ${testFunctionsPerFile} test functions = ${totalTestItems} total items`, + ); + console.log(`Total execution time: ${totalTime.toFixed(2)}ms`); + console.log(`Tree operations performed: ${treeRebuildCount}`); + console.log(`Search operations: ${totalSearchOperations}`); + console.log(`Average time per call: ${(totalCallTime / callCount).toFixed(2)}ms`); + console.log(`Results processed: ${numParameterizedResults}`); + + // Basic function call verification + assert.strictEqual(callCount, 1, 'Expected resolveExecution to be called once'); + + // EFFICIENCY VERIFICATION: Ensure minimal expensive operations + assert.strictEqual( + treeRebuildCount, + 0, + 'Expected ZERO tree rebuilds - efficient implementation should use cached lookups', + ); + + assert.strictEqual( + totalSearchOperations, + 0, + 'Expected ZERO linear search operations - efficient implementation should use direct lookups', + ); + + // Performance threshold verification - should be fast + assert.ok(totalTime < 100, `Function should complete quickly, took ${totalTime}ms (should be under 100ms)`); + + // Scalability check - time should not grow significantly with more results + const timePerResult = totalTime / numParameterizedResults; + assert.ok( + timePerResult < 10, + `Time per result should be minimal: ${timePerResult.toFixed(2)}ms per result (should be under 10ms)`, + ); + }); +}); diff --git a/src/test/testing/common/trackEnablement.unit.test.ts b/src/test/testing/common/trackEnablement.unit.test.ts deleted file mode 100644 index f1d8c94eed80..000000000000 --- a/src/test/testing/common/trackEnablement.unit.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -// 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 deleted file mode 100644 index 5b8498acb663..000000000000 --- a/src/test/testing/common/xUnitParser.unit.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -// 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 index 396d24221252..e259587ecccd 100644 --- a/src/test/testing/configuration.unit.test.ts +++ b/src/test/testing/configuration.unit.test.ts @@ -3,8 +3,6 @@ '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'; @@ -12,23 +10,22 @@ import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../cli import { IConfigurationService, IInstaller, - IOutputChannel, + ILogOutputChannel, IPythonSettings, - ITestingSettings, - Product + 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 { 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'; + ITestConfigurationManagerFactory, + ITestsHelper, +} from '../../client/testing/common/types'; +import { ITestingSettings } from '../../client/testing/configuration/types'; +import { UnitTestConfigurationService } from '../../client/testing/configuration'; suite('Unit Tests - ConfigurationService', () => { UNIT_TEST_PRODUCTS.forEach((product) => { @@ -46,7 +43,7 @@ suite('Unit Tests - ConfigurationService', () => { const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(undefined, typeMoq.MockBehavior.Strict); const configurationService = typeMoq.Mock.ofType<IConfigurationService>( undefined, - typeMoq.MockBehavior.Strict + typeMoq.MockBehavior.Strict, ); appShell = typeMoq.Mock.ofType<IApplicationShell>(undefined, typeMoq.MockBehavior.Strict); const outputChannel = typeMoq.Mock.ofType<OutputChannel>(undefined, typeMoq.MockBehavior.Strict); @@ -55,7 +52,7 @@ suite('Unit Tests - ConfigurationService', () => { factory = typeMoq.Mock.ofType<ITestConfigurationManagerFactory>(undefined, typeMoq.MockBehavior.Strict); testSettingsService = typeMoq.Mock.ofType<ITestConfigSettingsService>( undefined, - typeMoq.MockBehavior.Strict + typeMoq.MockBehavior.Strict, ); unitTestSettings = typeMoq.Mock.ofType<ITestingSettings>(); const pythonSettings = typeMoq.Mock.ofType<IPythonSettings>(undefined, typeMoq.MockBehavior.Strict); @@ -64,7 +61,7 @@ suite('Unit Tests - ConfigurationService', () => { configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .setup((c) => c.get(typeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer @@ -86,21 +83,18 @@ suite('Unit Tests - ConfigurationService', () => { 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)); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(ITestsHelper))).returns(() => new TestsHelper()); testConfigService = typeMoq.Mock.ofType( UnitTestConfigurationService, typeMoq.MockBehavior.Loose, true, - serviceContainer.object + serviceContainer.object, ); }); test('Enable Test when setting testing.promptToConfigure is enabled', async () => { const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( undefined, - typeMoq.MockBehavior.Strict + typeMoq.MockBehavior.Strict, ); configMgr .setup((c) => c.enable()) @@ -114,7 +108,7 @@ suite('Unit Tests - ConfigurationService', () => { const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( undefined, - typeMoq.MockBehavior.Strict + typeMoq.MockBehavior.Strict, ); workspaceService .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) @@ -136,7 +130,7 @@ suite('Unit Tests - ConfigurationService', () => { test('Enable Test when setting testing.promptToConfigure is disabled', async () => { const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( undefined, - typeMoq.MockBehavior.Strict + typeMoq.MockBehavior.Strict, ); configMgr .setup((c) => c.enable()) @@ -150,7 +144,7 @@ suite('Unit Tests - ConfigurationService', () => { const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( undefined, - typeMoq.MockBehavior.Strict + typeMoq.MockBehavior.Strict, ); workspaceService .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) @@ -164,7 +158,7 @@ suite('Unit Tests - ConfigurationService', () => { workspaceConfig .setup((w) => - w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)) + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)), ) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); @@ -179,7 +173,7 @@ suite('Unit Tests - ConfigurationService', () => { 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 + typeMoq.MockBehavior.Strict, ); configMgr .setup((c) => c.enable()) @@ -193,7 +187,7 @@ suite('Unit Tests - ConfigurationService', () => { const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( undefined, - typeMoq.MockBehavior.Strict + typeMoq.MockBehavior.Strict, ); workspaceService .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) @@ -209,7 +203,7 @@ suite('Unit Tests - ConfigurationService', () => { const updateFailError = new Error(errorMessage); workspaceConfig .setup((w) => - w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)) + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)), ) .returns(() => Promise.reject(updateFailError)) .verifiable(typeMoq.Times.once()); @@ -222,11 +216,11 @@ suite('Unit Tests - ConfigurationService', () => { workspaceService.verifyAll(); workspaceConfig.verifyAll(); }); - test('Select Test runner displays 3 items', async () => { + test('Select Test runner displays 2 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)) + .callback((items) => expect(items).be.lengthOf(2)) .verifiable(typeMoq.Times.once()); await testConfigService.target.selectTestRunner(placeHolder); @@ -234,10 +228,10 @@ suite('Unit Tests - ConfigurationService', () => { }); test('Ensure selected item is returned', async () => { const placeHolder = 'Some message'; - const indexes = [Product.unittest, Product.pytest, Product.nosetest]; + const indexes = [Product.unittest, Product.pytest]; appShell .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) - .callback((items) => expect(items).be.lengthOf(3)) + .callback((items) => expect(items).be.lengthOf(2)) .returns((items) => items[indexes.indexOf(product)]) .verifiable(typeMoq.Times.once()); @@ -245,7 +239,7 @@ suite('Unit Tests - ConfigurationService', () => { expect(selectedItem).to.be.equal(product); appShell.verifyAll(); }); - test('Ensure undefined is returned when nothing is seleted', async () => { + 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 }))) @@ -256,212 +250,23 @@ suite('Unit Tests - ConfigurationService', () => { 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); + test('Correctly returns hasConfiguredTests', () => { + let enabled = 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()); + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => enabled); - 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(); + expect(testConfigService.target.hasConfiguredTests(workspaceUri)).to.equal(false); + enabled = true; + expect(testConfigService.target.hasConfiguredTests(workspaceUri)).to.equal(true); }); 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 + typeMoq.MockBehavior.Strict, ); workspaceConfig .setup((w) => w.get(typeMoq.It.isAny())) @@ -482,13 +287,13 @@ suite('Unit Tests - ConfigurationService', () => { .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) .returns(() => { selectTestRunnerInvoked = true; - return Promise.resolve(product as any); + return Promise.resolve(product); }); const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>(); factory .setup((f) => - f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()) + f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()), ) .returns(() => configMgr.object) .verifiable(typeMoq.Times.once()); diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts new file mode 100644 index 000000000000..d7a1313df591 --- /dev/null +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { PytestInstallationHelper } from '../../../client/testing/configuration/pytestInstallationHelper'; +import * as envExtApi from '../../../client/envExt/api.internal'; + +suite('PytestInstallationHelper', () => { + let appShell: TypeMoq.IMock<IApplicationShell>; + let helper: PytestInstallationHelper; + let useEnvExtensionStub: sinon.SinonStub; + let getEnvExtApiStub: sinon.SinonStub; + let getEnvironmentStub: sinon.SinonStub; + + const workspaceUri = Uri.file('/test/workspace'); + + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + helper = new PytestInstallationHelper(appShell.object); + + useEnvExtensionStub = sinon.stub(envExtApi, 'useEnvExtension'); + getEnvExtApiStub = sinon.stub(envExtApi, 'getEnvExtApi'); + getEnvironmentStub = sinon.stub(envExtApi, 'getEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('promptToInstallPytest should return false if user selects ignore', async () => { + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Ignore')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should return false if user cancels', async () => { + 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()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('isEnvExtensionAvailable should return result from useEnvExtension', () => { + useEnvExtensionStub.returns(true); + + const result = helper.isEnvExtensionAvailable(); + + expect(result).to.be.true; + expect(useEnvExtensionStub.calledOnce).to.be.true; + }); + + test('promptToInstallPytest should return false if env extension not available', async () => { + useEnvExtensionStub.returns(false); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should attempt installation when env extension is available', async () => { + useEnvExtensionStub.returns(true); + + const mockEnvironment = { envId: { id: 'test-env', managerId: 'test-manager' } }; + const mockEnvExtApi = { + managePackages: sinon.stub().resolves(), + }; + + getEnvExtApiStub.resolves(mockEnvExtApi); + getEnvironmentStub.resolves(mockEnvironment); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.true; + expect(mockEnvExtApi.managePackages.calledOnceWithExactly(mockEnvironment, { install: ['pytest'] })).to.be.true; + appShell.verifyAll(); + }); +}); diff --git a/src/test/testing/configurationFactory.unit.test.ts b/src/test/testing/configurationFactory.unit.test.ts index d19484cdb32b..493dfcc00b95 100644 --- a/src/test/testing/configurationFactory.unit.test.ts +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -7,16 +7,14 @@ 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 { IInstaller, ILogOutputChannel, Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; -import { TEST_OUTPUT_CHANNEL } from '../../client/testing/common/constants'; +import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/common/types'; 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'; +import * as pytest from '../../client/testing/configuration/pytest/testConfigurationManager'; +import * as unittest from '../../client/testing/configuration/unittest/testConfigurationManager'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Unit Tests - ConfigurationManagerFactory', () => { let factory: ITestConfigurationManagerFactory; @@ -26,9 +24,7 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { 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(ILogOutputChannel))).returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer .setup((c) => c.get(typeMoq.It.isValue(ITestConfigSettingsService))) @@ -43,8 +39,4 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { 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 deleted file mode 100644 index 2b8172b2a527..000000000000 --- a/src/test/testing/debugger.test.ts +++ /dev/null @@ -1,260 +0,0 @@ -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 deleted file mode 100644 index d529dc25b183..000000000000 --- a/src/test/testing/display/main.unit.test.ts +++ /dev/null @@ -1,516 +0,0 @@ -// 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 deleted file mode 100644 index 14de3b7f66e4..000000000000 --- a/src/test/testing/display/picker.functional.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -// 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 deleted file mode 100644 index d0b77e003890..000000000000 --- a/src/test/testing/display/picker.unit.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -// 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 deleted file mode 100644 index bd8acf2b17c1..000000000000 --- a/src/test/testing/explorer/explorerTestData.ts +++ /dev/null @@ -1,261 +0,0 @@ -// 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 deleted file mode 100644 index e38715a41958..000000000000 --- a/src/test/testing/explorer/failedTestHandler.unit.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -// 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 deleted file mode 100644 index 403c3a8438ee..000000000000 --- a/src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -// 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 deleted file mode 100644 index 423665c35d2a..000000000000 --- a/src/test/testing/explorer/testTreeViewItem.unit.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index e38dedec2f8c..000000000000 --- a/src/test/testing/explorer/testTreeViewProvider.unit.test.ts +++ /dev/null @@ -1,995 +0,0 @@ -// 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 deleted file mode 100644 index 1222a55a238e..000000000000 --- a/src/test/testing/explorer/treeView.unit.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index 3f9ffbf44c89..000000000000 --- a/src/test/testing/helper.ts +++ /dev/null @@ -1,78 +0,0 @@ -// 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 deleted file mode 100644 index 147fbee07733..000000000000 --- a/src/test/testing/main.unit.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index b41fc3e2da6a..000000000000 --- a/src/test/testing/mocks.ts +++ /dev/null @@ -1,136 +0,0 @@ -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 deleted file mode 100644 index be420ec7ab76..000000000000 --- a/src/test/testing/navigation/commandHandlers.unit.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -// 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 deleted file mode 100644 index 4147a2e3def8..000000000000 --- a/src/test/testing/navigation/fileNavigator.unit.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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 deleted file mode 100644 index c883a3fb97a4..000000000000 --- a/src/test/testing/navigation/functionNavigator.unit.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -// 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 deleted file mode 100644 index bd0ec9aea0d5..000000000000 --- a/src/test/testing/navigation/helper.unit.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// 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 deleted file mode 100644 index 85687b9c606a..000000000000 --- a/src/test/testing/navigation/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -// 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 deleted file mode 100644 index df06fba2b6b4..000000000000 --- a/src/test/testing/navigation/suiteNavigator.unit.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -// 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 deleted file mode 100644 index 744ad16b0ab2..000000000000 --- a/src/test/testing/navigation/symbolNavigator.unit.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -// 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 deleted file mode 100644 index 0b5224176b1a..000000000000 --- a/src/test/testing/nosetest/nosetest.argsService.unit.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index a6dfb25e46cb..000000000000 --- a/src/test/testing/nosetest/nosetest.discovery.unit.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -// 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 deleted file mode 100644 index 74b21f87b2e8..000000000000 --- a/src/test/testing/nosetest/nosetest.disovery.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -// 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.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 deleted file mode 100644 index e9b0c84ee6cf..000000000000 --- a/src/test/testing/nosetest/nosetest.run.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -// 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 deleted file mode 100644 index 305ee65d75ab..000000000000 --- a/src/test/testing/nosetest/nosetest.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 86d6a0878758..000000000000 --- a/src/test/testing/pytest/pytest.argsService.unit.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -// 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 deleted file mode 100644 index 3ee16265b429..000000000000 --- a/src/test/testing/pytest/pytest.discovery.test.ts +++ /dev/null @@ -1,1016 +0,0 @@ -// 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 { IWindowsStoreInterpreter } from '../../../client/interpreter/locators/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -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 { 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(IWindowsStoreInterpreter) windowsStoreInterpreter: IWindowsStoreInterpreter, - @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.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 deleted file mode 100644 index d4eca020f59c..000000000000 --- a/src/test/testing/pytest/pytest.run.test.ts +++ /dev/null @@ -1,646 +0,0 @@ -// 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 { IWindowsStoreInterpreter } from '../../../client/interpreter/locators/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; -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(IWindowsStoreInterpreter) windowsStoreInterpreter: IWindowsStoreInterpreter, - @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.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 deleted file mode 100644 index 489612b889e9..000000000000 --- a/src/test/testing/pytest/pytest.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 8396db04bf88..000000000000 --- a/src/test/testing/pytest/pytest.testMessageService.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -// 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 deleted file mode 100644 index 33114b43569a..000000000000 --- a/src/test/testing/pytest/pytest_run_tests_data.ts +++ /dev/null @@ -1,472 +0,0 @@ -// 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 deleted file mode 100644 index 30bdf9d1cec4..000000000000 --- a/src/test/testing/pytest/pytest_unittest_parser_data.ts +++ /dev/null @@ -1,2087 +0,0 @@ -// 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 deleted file mode 100644 index ea46e70c4bb4..000000000000 --- a/src/test/testing/pytest/services/discoveryService.unit.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -// 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 deleted file mode 100644 index fe6a82e43b63..000000000000 --- a/src/test/testing/rediscover.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index 824ef1c1fa4a..000000000000 --- a/src/test/testing/results.ts +++ /dev/null @@ -1,560 +0,0 @@ -// 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 index 0a9df7793822..231716b653ba 100644 --- a/src/test/testing/serviceRegistry.ts +++ b/src/test/testing/serviceRegistry.ts @@ -1,198 +1,34 @@ // 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 { ITestsHelper } from '../../client/testing/common/types'; 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... } + 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() { + public registerTestsHelper(): void { 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); + public registerInterpreterStorageTypes(): void { 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 deleted file mode 100644 index c10bc5f1c69c..000000000000 --- a/src/test/testing/stoppingDiscoverAndTest.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -// 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/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts new file mode 100644 index 000000000000..643ea17903e6 --- /dev/null +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { buildErrorNodeOptions } from '../../../../client/testing/testController/common/utils'; + +suite('buildErrorNodeOptions - missing module detection', () => { + const workspaceUri = Uri.file('/test/workspace'); + + test('Should detect pytest ModuleNotFoundError and show missing module label', () => { + const errorMessage = + 'Traceback (most recent call last):\n File "<string>", line 1, in <module>\n import pytest\nModuleNotFoundError: No module named \'pytest\''; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: pytest [workspace]'); + expect(result.error).to.equal( + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect pytest ImportError and show missing module label', () => { + const errorMessage = 'ImportError: No module named pytest'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: pytest [workspace]'); + expect(result.error).to.equal( + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect other missing modules and show module name in label', () => { + const errorMessage = + "bob\\test_bob.py:3: in <module>\n import requests\nE ModuleNotFoundError: No module named 'requests'\n=========================== short test summary info"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: requests [workspace]'); + expect(result.error).to.equal( + "The module 'requests' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect missing module with double quotes', () => { + const errorMessage = 'ModuleNotFoundError: No module named "numpy"'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: numpy [workspace]'); + expect(result.error).to.equal( + "The module 'numpy' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for non-module-related errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should detect missing module for unittest errors', () => { + const errorMessage = "ModuleNotFoundError: No module named 'pandas'"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Missing Module: pandas [workspace]'); + expect(result.error).to.equal( + "The module 'pandas' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for unittest non-module errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should use project name in label when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', 'my-project'); + + expect(result.label).to.equal('Unittest Discovery Error [my-project]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use project name in label for pytest when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest', 'ada'); + + expect(result.label).to.equal('pytest Discovery Error [ada]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use folder name when projectName is undefined', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', undefined); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + }); +}); diff --git a/src/test/testing/testController/common/projectTestExecution.unit.test.ts b/src/test/testing/testController/common/projectTestExecution.unit.test.ts new file mode 100644 index 000000000000..1cce2d1a8ce0 --- /dev/null +++ b/src/test/testing/testController/common/projectTestExecution.unit.test.ts @@ -0,0 +1,740 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { + CancellationToken, + CancellationTokenSource, + TestRun, + TestRunProfile, + TestRunProfileKind, + TestRunRequest, + Uri, +} from 'vscode'; +import { + createMockDependencies, + createMockProjectAdapter, + createMockTestItem, + createMockTestItemWithoutUri, + createMockTestRun, +} from '../testMocks'; +import { + executeTestsForProject, + executeTestsForProjects, + findProjectForTestItem, + getTestCaseNodesRecursive, + groupTestItemsByProject, + setupCoverageForProjects, +} from '../../../../client/testing/testController/common/projectTestExecution'; +import * as telemetry from '../../../../client/telemetry'; +import * as envExtApi from '../../../../client/envExt/api.internal'; + +suite('Project Test Execution', () => { + let sandbox: sinon.SinonSandbox; + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + // Default to disabled env extension for path-based fallback tests + useEnvExtensionStub = sandbox.stub(envExtApi, 'useEnvExtension').returns(false); + }); + + teardown(() => { + sandbox.restore(); + }); + + // ===== findProjectForTestItem Tests ===== + + suite('findProjectForTestItem', () => { + test('should return undefined when test item has no URI', async () => { + // Mock + const item = createMockTestItemWithoutUri('test1'); + const projects = [createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' })]; + + // Run + const result = await findProjectForTestItem(item, projects); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return matching project when item path is within project directory', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should return undefined when item path is outside all project directories', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return most specific (deepest) project when nested projects exist', async () => { + // Mock - parent and child project with overlapping paths + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await findProjectForTestItem(item, [parentProject, childProject]); + + // Assert - should match child (longer path) not parent + expect(result).to.equal(childProject); + }); + + test('should return most specific project regardless of input order', async () => { + // Mock - same as above but different order + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run - pass child first, then parent + const result = await findProjectForTestItem(item, [childProject, parentProject]); + + // Assert - order shouldn't affect result + expect(result).to.equal(childProject); + }); + + test('should match item at project root level', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should use env extension API when available', async () => { + // Enable env extension + useEnvExtensionStub.returns(true); + + // Mock the env extension API + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + const mockEnvApi = { + getPythonProject: sandbox.stub().returns({ uri: project.projectUri }), + }; + sandbox.stub(envExtApi, 'getEnvExtApi').resolves(mockEnvApi as any); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + expect(mockEnvApi.getPythonProject.calledOnceWith(item.uri)).to.be.true; + }); + + test('should fall back to path matching when env extension API is unavailable', async () => { + // Env extension enabled but throws + useEnvExtensionStub.returns(true); + sandbox.stub(envExtApi, 'getEnvExtApi').rejects(new Error('API unavailable')); + + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert - should still work via fallback + expect(result).to.equal(project); + }); + }); + + // ===== groupTestItemsByProject Tests ===== + + suite('groupTestItemsByProject', () => { + test('should group single test item to its matching project', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.project).to.equal(project); + expect(entry.items).to.deep.equal([item]); + }); + + test('should aggregate multiple items belonging to same project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj/tests/test1.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/tests/test2.py'); + const item3 = createMockTestItem('test3', '/workspace/proj/test3.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [project]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.items).to.have.length(3); + expect(new Set(entry.items)).to.deep.equal(new Set([item1, item2, item3])); + }); + + test('should separate items into groups by their owning project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const item3 = createMockTestItem('test3', '/workspace/proj1/other_test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [proj1, proj2]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(2); + const proj1Entry = result.get(proj1.projectUri.toString()); + const proj2Entry = result.get(proj2.projectUri.toString()); + expect(proj1Entry?.items).to.have.length(2); + expect(new Set(proj1Entry?.items)).to.deep.equal(new Set([item1, item3])); + expect(proj2Entry?.items).to.deep.equal([item2]); + }); + + test('should return empty map when no test items provided', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should exclude items that do not match any project path', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should assign item to most specific (deepest) project for nested paths', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/parent/child/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await groupTestItemsByProject([item], [parentProject, childProject]); + + // Assert + expect(result.size).to.equal(1); + const entry = result.get(childProject.projectUri.toString()); + expect(entry?.project).to.equal(childProject); + expect(entry?.items).to.deep.equal([item]); + }); + + test('should omit projects that have no matching test items', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj1/test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item], [proj1, proj2]); + + // Assert + expect(result.size).to.equal(1); + expect(result.has(proj1.projectUri.toString())).to.be.true; + expect(result.has(proj2.projectUri.toString())).to.be.false; + }); + }); + + // ===== getTestCaseNodesRecursive Tests ===== + + suite('getTestCaseNodesRecursive', () => { + test('should return single item when it is a leaf node with no children', () => { + // Mock + const item = createMockTestItem('test_func', '/test.py'); + + // Run + const result = getTestCaseNodesRecursive(item); + + // Assert + expect(result).to.deep.equal([item]); + }); + + test('should return all leaf nodes from single-level nested structure', () => { + // Mock + const leaf1 = createMockTestItem('test_method1', '/test.py'); + const leaf2 = createMockTestItem('test_method2', '/test.py'); + const classItem = createMockTestItem('TestClass', '/test.py', [leaf1, leaf2]); + + // Run + const result = getTestCaseNodesRecursive(classItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should traverse deeply nested structure to find all leaf nodes', () => { + // Mock - 3 levels deep: file → class → inner class → test + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const innerClass = createMockTestItem('InnerClass', '/test.py', [leaf2]); + const outerClass = createMockTestItem('OuterClass', '/test.py', [leaf1, innerClass]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [outerClass]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should collect leaves from multiple sibling branches', () => { + // Mock - multiple test classes at same level + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const leaf3 = createMockTestItem('test3', '/test.py'); + const class1 = createMockTestItem('Class1', '/test.py', [leaf1]); + const class2 = createMockTestItem('Class2', '/test.py', [leaf2, leaf3]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [class1, class2]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(3); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2, leaf3])); + }); + }); + + // ===== executeTestsForProject Tests ===== + + suite('executeTestsForProject', () => { + test('should call executionAdapter.runTests with project URI and mapped test IDs', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'test_file.py::test1'); + const testItem = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [testItem], runMock.object, request, deps); + + // Assert + expect(project.executionAdapterStub.calledOnce).to.be.true; + const callArgs = project.executionAdapterStub.firstCall.args; + expect(callArgs[0].fsPath).to.equal(project.projectUri.fsPath); // uri + expect(callArgs[1]).to.deep.equal(['test_file.py::test1']); // testCaseIds + expect(callArgs[7]).to.equal(project); // project + }); + + test('should mark all leaf test items as started in the test run', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - both items marked as started + runMock.verify((r) => r.started(item1), typemoq.Times.once()); + runMock.verify((r) => r.started(item2), typemoq.Times.once()); + }); + + test('should resolve test IDs via resultResolver.vsIdToRunId mapping', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'path/to/test1'); + project.resultResolver.vsIdToRunId.set('test2', 'path/to/test2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - use Set for order-agnostic comparison + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(new Set(passedTestIds)).to.deep.equal(new Set(['path/to/test1', 'path/to/test2'])); + }); + + test('should skip execution when no items have vsIdToRunId mappings', async () => { + // Mock - no mappings set, so lookups return undefined + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('unmapped_test', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item], runMock.object, request, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should recursively expand nested test items to find leaf nodes', async () => { + // Mock - class containing two test methods + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const leaf1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const leaf2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const classItem = createMockTestItem('TestClass', '/workspace/proj/test.py', [leaf1, leaf2]); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [classItem], runMock.object, request, deps); + + // Assert - leaf nodes marked as started, not the parent class + runMock.verify((r) => r.started(leaf1), typemoq.Times.once()); + runMock.verify((r) => r.started(leaf2), typemoq.Times.once()); + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(passedTestIds).to.have.length(2); + }); + }); + + // ===== executeTestsForProjects Tests ===== + + suite('executeTestsForProjects', () => { + let telemetryStub: sinon.SinonStub; + + setup(() => { + telemetryStub = sandbox.stub(telemetry, 'sendTelemetryEvent'); + }); + + test('should return immediately when empty projects array provided', async () => { + // Mock + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([], [], runMock.object, request, token, deps); + + // Assert - no telemetry sent since no projects executed + expect(telemetryStub.called).to.be.false; + }); + + test('should skip execution when cancellation requested before start', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); // Pre-cancel + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, tokenSource.token, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should execute tests for each project when multiple projects provided', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - both projects had their execution adapters called + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should emit telemetry event for each project execution', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - telemetry sent twice (once per project) + expect(telemetryStub.callCount).to.equal(2); + }); + + test('should stop processing remaining projects when cancellation requested mid-execution', async () => { + // Mock + const tokenSource = new CancellationTokenSource(); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + // First project triggers cancellation during its execution + proj1.executionAdapterStub.callsFake(async () => { + tokenSource.cancel(); + }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects( + [proj1, proj2], + [item1, item2], + runMock.object, + request, + tokenSource.token, + deps, + ); + + // Assert - first project executed, second may be skipped due to cancellation check + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should continue executing remaining projects when one project fails', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.executionAdapterStub.rejects(new Error('Execution failed')); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run - should not throw + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - second project still executed despite first failing + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should configure loadDetailedCoverage callback when run profile is Coverage', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - loadDetailedCoverage callback was configured + expect(profileMock.loadDetailedCoverage).to.not.be.undefined; + }); + + test('should include debugging=true in telemetry when run profile is Debug', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Debug } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - telemetry contains debugging=true + expect(telemetryStub.calledOnce).to.be.true; + const telemetryProps = telemetryStub.firstCall.args[2]; + expect(telemetryProps.debugging).to.be.true; + }); + }); + + // ===== setupCoverageForProjects Tests ===== + + suite('setupCoverageForProjects', () => { + test('should configure loadDetailedCoverage callback when profile kind is Coverage', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.a('function'); + }); + + test('should leave loadDetailedCoverage undefined when profile kind is Run', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Run, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.undefined; + }); + + test('should return coverage data from detailedCoverageMap when loadDetailedCoverage is called', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const mockCoverageDetails = [{ line: 1, executed: true }]; + // Use Uri.fsPath as the key to match the implementation's lookup + const fileUri = Uri.file('/workspace/proj/file.py'); + project.resultResolver.detailedCoverageMap.set(fileUri.fsPath, mockCoverageDetails as any); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call the configured callback + const fileCoverage = { uri: fileUri }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal(mockCoverageDetails); + }); + + test('should return empty array when file has no coverage data in map', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call callback for file not in map + const fileCoverage = { uri: Uri.file('/workspace/proj/uncovered_file.py') }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal([]); + }); + + test('should route to correct project when multiple projects have coverage data', async () => { + // Mock - two projects with different coverage data + const project1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const project2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + const coverage1 = [{ line: 1, executed: true }]; + const coverage2 = [{ line: 2, executed: false }]; + const file1Uri = Uri.file('/workspace/proj1/file1.py'); + const file2Uri = Uri.file('/workspace/proj2/file2.py'); + project1.resultResolver.detailedCoverageMap.set(file1Uri.fsPath, coverage1 as any); + project2.resultResolver.detailedCoverageMap.set(file2Uri.fsPath, coverage2 as any); + + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage with both projects + setupCoverageForProjects(request, [project1, project2]); + + // Assert - can get coverage from both projects through single callback + const result1 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file1Uri } as any, + {} as CancellationToken, + ); + const result2 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file2Uri } as any, + {} as CancellationToken, + ); + + expect(result1).to.deep.equal(coverage1); + expect(result2).to.deep.equal(coverage2); + }); + }); +}); diff --git a/src/test/testing/testController/common/projectUtils.unit.test.ts b/src/test/testing/testController/common/projectUtils.unit.test.ts new file mode 100644 index 000000000000..75f399e89fc0 --- /dev/null +++ b/src/test/testing/testController/common/projectUtils.unit.test.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { + getProjectId, + createProjectDisplayName, + parseVsId, + PROJECT_ID_SEPARATOR, +} from '../../../../client/testing/testController/common/projectUtils'; + +suite('Project Utils Tests', () => { + suite('getProjectId', () => { + test('should return URI string representation', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + expect(id).to.equal(uri.toString()); + }); + + test('should be consistent for same URI', () => { + const uri = Uri.file('/workspace/project'); + + const id1 = getProjectId(uri); + const id2 = getProjectId(uri); + + expect(id1).to.equal(id2); + }); + + test('should be different for different URIs', () => { + const uri1 = Uri.file('/workspace/project1'); + const uri2 = Uri.file('/workspace/project2'); + + const id1 = getProjectId(uri1); + const id2 = getProjectId(uri2); + + expect(id1).to.not.equal(id2); + }); + + test('should handle Windows paths', () => { + const uri = Uri.file('C:\\workspace\\project'); + + const id = getProjectId(uri); + + expect(id).to.be.a('string'); + expect(id).to.have.length.greaterThan(0); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should match Python Environments extension format', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + // Should match how Python Environments extension keys projects + expect(id).to.equal(uri.toString()); + expect(typeof id).to.equal('string'); + }); + }); + + suite('createProjectDisplayName', () => { + test('should format name with major.minor version', () => { + const result = createProjectDisplayName('MyProject', '3.11.2'); + + expect(result).to.equal('MyProject (Python 3.11)'); + }); + + test('should handle version with patch and pre-release', () => { + const result = createProjectDisplayName('MyProject', '3.12.0rc1'); + + expect(result).to.equal('MyProject (Python 3.12)'); + }); + + test('should handle version with only major.minor', () => { + const result = createProjectDisplayName('MyProject', '3.10'); + + expect(result).to.equal('MyProject (Python 3.10)'); + }); + + test('should handle invalid version format gracefully', () => { + const result = createProjectDisplayName('MyProject', 'invalid-version'); + + expect(result).to.equal('MyProject (Python invalid-version)'); + }); + + test('should handle empty version string', () => { + const result = createProjectDisplayName('MyProject', ''); + + expect(result).to.equal('MyProject (Python )'); + }); + + test('should handle version with single digit', () => { + const result = createProjectDisplayName('MyProject', '3'); + + expect(result).to.equal('MyProject (Python 3)'); + }); + + test('should handle project name with special characters', () => { + const result = createProjectDisplayName('My-Project_123', '3.11.5'); + + expect(result).to.equal('My-Project_123 (Python 3.11)'); + }); + + test('should handle empty project name', () => { + const result = createProjectDisplayName('', '3.11.2'); + + expect(result).to.equal(' (Python 3.11)'); + }); + }); + + suite('parseVsId', () => { + test('should parse project-scoped ID correctly', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle legacy ID without project scope', () => { + const vsId = 'test_file.py'; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.be.undefined; + expect(runId).to.equal('test_file.py'); + }); + + test('should handle runId containing separator', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_class::test_method`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_class::test_method'); + }); + + test('should handle empty project ID', () => { + const vsId = `${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal(''); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle empty runId', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal(''); + }); + + test('should handle ID with file path', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}/workspace/tests/test_file.py`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal('/workspace/tests/test_file.py'); + }); + + test('should handle Windows file paths', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}C:\\workspace\\tests\\test_file.py`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('C:\\workspace\\tests\\test_file.py'); + }); + }); + + suite('Integration Tests', () => { + test('should generate unique IDs for different URIs', () => { + const uris = [ + Uri.file('/workspace/a'), + Uri.file('/workspace/b'), + Uri.file('/workspace/c'), + Uri.file('/workspace/d'), + Uri.file('/workspace/e'), + ]; + + const ids = uris.map((uri) => getProjectId(uri)); + const uniqueIds = new Set(ids); + + expect(uniqueIds.size).to.equal(uris.length, 'All IDs should be unique'); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should create complete vsId and parse it back', () => { + const projectUri = Uri.file('/workspace/myproject'); + const projectId = getProjectId(projectUri); + const runId = 'tests/test_module.py::TestClass::test_method'; + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}${runId}`; + + const [parsedProjectId, parsedRunId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(parsedRunId).to.equal(runId); + }); + + test('should match Python Environments extension URI format', () => { + const uri = Uri.file('/workspace/project'); + + const projectId = getProjectId(uri); + + // Should be string representation of URI + expect(projectId).to.equal(uri.toString()); + expect(typeof projectId).to.equal('string'); + }); + }); +}); diff --git a/src/test/testing/testController/common/testCoverageHandler.unit.test.ts b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts new file mode 100644 index 000000000000..a81aed591128 --- /dev/null +++ b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts @@ -0,0 +1,502 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, FileCoverage } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import { TestCoverageHandler } from '../../../../client/testing/testController/common/testCoverageHandler'; +import { CoveragePayload } from '../../../../client/testing/testController/common/types'; + +suite('TestCoverageHandler', () => { + let coverageHandler: TestCoverageHandler; + let runInstanceMock: typemoq.IMock<TestRun>; + + setup(() => { + coverageHandler = new TestCoverageHandler(); + runInstanceMock = typemoq.Mock.ofType<TestRun>(); + }); + + suite('processCoverage', () => { + test('should return empty map for undefined result', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: undefined, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.never()); + }); + + test('should create FileCoverage for each file', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + '/path/to/file2.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + }, + error: '', + }; + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test('should call runInstance.addCoverage with correct FileCoverage', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + assert.strictEqual(capturedCoverage!.uri.fsPath, Uri.file('/path/to/file.py').fsPath); + }); + + test('should return detailed coverage map with correct keys', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + '/path/to/file2.py': { + lines_covered: [5, 6, 7], + lines_missed: [], + executed_branches: 3, + total_branches: 3, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 2); + assert.ok(result.has(Uri.file('/path/to/file1.py').fsPath)); + assert.ok(result.has(Uri.file('/path/to/file2.py').fsPath)); + }); + + test('should handle empty coverage data', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: {}, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + }); + + test('should handle file with no covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only missed lines + }); + + test('should handle file with no missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 5, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only covered lines + }); + + test('should handle undefined lines_covered', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: undefined as any, + lines_missed: [1, 2], + executed_branches: 0, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only missed lines + }); + + test('should handle undefined lines_missed', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2], + lines_missed: undefined as any, + executed_branches: 2, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only covered lines + }); + }); + + suite('createFileCoverage', () => { + test('should handle line coverage only when totalBranches is -1', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 0, + total_branches: -1, // Branch coverage disabled + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Branch coverage should not be included + assert.strictEqual((capturedCoverage as any).branchCoverage, undefined); + }); + + test('should include branch coverage when available', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4], + executed_branches: 7, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Should have branch coverage + assert.ok((capturedCoverage as any).branchCoverage); + }); + + test('should calculate line coverage counts correctly', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3, 4, 5], + lines_missed: [6, 7], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // 5 covered out of 7 total (5 covered + 2 missed) + assert.strictEqual((capturedCoverage as any).statementCoverage.covered, 5); + assert.strictEqual((capturedCoverage as any).statementCoverage.total, 7); + }); + }); + + suite('createDetailedCoverage', () => { + test('should create StatementCoverage for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be covered (true) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, true); + }); + }); + + test('should create StatementCoverage for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be NOT covered (false) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, false); + }); + }); + + test('should convert 1-indexed to 0-indexed line numbers for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 5, 10], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 1 should map to range starting at line 0 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 0); + // Line 5 should map to range starting at line 4 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4); + // Line 10 should map to range starting at line 9 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9); + }); + + test('should convert 1-indexed to 0-indexed line numbers for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [3, 7, 12], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 3 should map to range starting at line 2 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 2); + // Line 7 should map to range starting at line 6 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 6); + // Line 12 should map to range starting at line 11 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 11); + }); + + test('should handle large line numbers', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1000, 5000, 10000], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // Verify conversion is correct for large numbers + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 999); + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4999); + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9999); + }); + + test('should create detailed coverage with both covered and missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 3, 5], + lines_missed: [2, 4, 6], + executed_branches: 3, + total_branches: 6, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 6); // 3 covered + 3 missed + + // Count covered vs not covered + const covered = detailedCoverage!.filter((c) => (c as any).executed === true); + const notCovered = detailedCoverage!.filter((c) => (c as any).executed === false); + + assert.strictEqual(covered.length, 3); + assert.strictEqual(notCovered.length, 3); + }); + + test('should set range to cover entire line', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + const coverage = detailedCoverage![0] as any; + // Start at column 0 + assert.strictEqual(coverage.location.start.character, 0); + // End at max safe integer (entire line) + assert.strictEqual(coverage.location.end.character, Number.MAX_SAFE_INTEGER); + }); + }); +}); diff --git a/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts new file mode 100644 index 000000000000..458e3d984405 --- /dev/null +++ b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, CancellationToken, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestDiscoveryHandler } from '../../../../client/testing/testController/common/testDiscoveryHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { DiscoveredTestPayload, DiscoveredTestNode } from '../../../../client/testing/testController/common/types'; +import { TestProvider } from '../../../../client/testing/types'; +import * as utils from '../../../../client/testing/testController/common/utils'; +import * as testItemUtilities from '../../../../client/testing/testController/common/testItemUtilities'; + +suite('TestDiscoveryHandler', () => { + let discoveryHandler: TestDiscoveryHandler; + let testControllerMock: typemoq.IMock<TestController>; + let testItemIndexMock: typemoq.IMock<TestItemIndex>; + let testItemCollectionMock: typemoq.IMock<TestItemCollection>; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + + setup(() => { + discoveryHandler = new TestDiscoveryHandler(); + testControllerMock = typemoq.Mock.ofType<TestController>(); + testItemIndexMock = typemoq.Mock.ofType<TestItemIndex>(); + testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + + // Setup default test controller items mock + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + testItemCollectionMock.setup((x) => x.delete(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + workspaceUri = Uri.file('/foo/bar'); + testProvider = 'pytest'; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processDiscovery', () => { + test('should handle null payload gracefully', () => { + discoveryHandler.processDiscovery( + null as any, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should not throw and should not call populateTestTree + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.never()); + }); + + test('should call populateTestTree with correct params on success', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + + // Setup map getters for populateTestTree + const mockRunIdMap = new Map(); + const mockVSidMap = new Map(); + const mockVStoRunMap = new Map(); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => mockRunIdMap); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => mockVSidMap); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => mockVStoRunMap); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + assert.ok(populateTestTreeStub.calledOnce); + sinon.assert.calledWith( + populateTestTreeStub, + testControllerMock.object, + tests, + undefined, + sinon.match.any, + cancelationToken, + ); + }); + + test('should clear index before populating', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + + const clearSpy = sinon.spy(); + testItemIndexMock.setup((x) => x.clear()).callback(clearSpy); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(clearSpy.calledOnce); + }); + + test('should handle error status and create error node', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Error message 1', 'Error message 2'], + }; + + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(createErrorNodeSpy.calledOnce); + assert.ok( + createErrorNodeSpy.calledWith(testControllerMock.object, workspaceUri, payload.error, testProvider), + ); + }); + + test('should handle both errors and tests in same payload', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Partial error'], + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should create error node AND populate test tree + assert.ok(createErrorNodeSpy.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + + test('should delete error node on successful discovery', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const deleteSpy = sinon.spy(); + // Reset and reconfigure the collection mock to capture delete call + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.delete(typemoq.It.isAny())) + .callback(deleteSpy) + .returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(deleteSpy.calledOnce); + assert.ok(deleteSpy.calledWith(`DiscoveryError:${workspaceUri.fsPath}`)); + }); + + test('should respect cancellation token', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Verify token was passed to populateTestTree + assert.ok(populateTestTreeStub.calledOnce); + const lastArg = populateTestTreeStub.getCall(0).args[4]; + assert.strictEqual(lastArg, cancelationToken); + }); + + test('should handle null tests in payload', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests: null as any, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should still call populateTestTree with null + assert.ok(populateTestTreeStub.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + }); + + suite('createErrorNode', () => { + test('should create error with correct message for pytest', () => { + const error = ['Error line 1', 'Error line 2']; + testProvider = 'pytest'; + + const buildErrorNodeOptionsStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(buildErrorNodeOptionsStub.calledOnce); + assert.ok(createErrorTestItemStub.calledOnce); + assert.ok(mockErrorItem.error !== null); + }); + + test('should create error with correct message for unittest', () => { + const error = ['Unittest error']; + testProvider = 'unittest'; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error !== null); + }); + + test('should set markdown error label correctly', () => { + const error = ['Test error']; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error); + assert.strictEqual( + (mockErrorItem.error as any).value, + '[Show output](command:python.viewOutput) to view error logs', + ); + assert.strictEqual((mockErrorItem.error as any).isTrusted, true); + }); + + test('should handle undefined error array', () => { + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, undefined, testProvider); + + // Should not throw + assert.ok(mockErrorItem.error !== null); + }); + + test('should reuse existing error node if present', () => { + const error = ['Error']; + + // Create a proper object with settable error property + const existingErrorItem: any = { + id: `DiscoveryError:${workspaceUri.fsPath}`, + error: null, + canResolveChildren: false, + tags: [], + }; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: `DiscoveryError:${workspaceUri.fsPath}`, + label: 'Error Label', + error: 'Error Message', + }); + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem'); + + // Reset and setup collection to return existing item + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.get(`DiscoveryError:${workspaceUri.fsPath}`)) + .returns(() => existingErrorItem); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Should not create a new error item + assert.ok(createErrorTestItemStub.notCalled); + // Should still update the error property + assert.ok(existingErrorItem.error !== null); + }); + + test('should handle multiple error messages', () => { + const error = ['Error 1', 'Error 2', 'Error 3']; + + const buildStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Verify the error messages are joined + const expectedMessage = sinon.match((value: string) => { + return value.includes('Error 1') && value.includes('Error 2') && value.includes('Error 3'); + }); + sinon.assert.calledWith(buildStub, workspaceUri, expectedMessage, testProvider); + }); + }); +}); diff --git a/src/test/testing/testController/common/testExecutionHandler.unit.test.ts b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts new file mode 100644 index 000000000000..c6be4548c192 --- /dev/null +++ b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts @@ -0,0 +1,922 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, TestRun, TestMessage, Uri, Range, TestItemCollection, MarkdownString } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestExecutionHandler } from '../../../../client/testing/testController/common/testExecutionHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { ExecutionTestPayload } from '../../../../client/testing/testController/common/types'; + +suite('TestExecutionHandler', () => { + let executionHandler: TestExecutionHandler; + let testControllerMock: typemoq.IMock<TestController>; + let testItemIndexMock: typemoq.IMock<TestItemIndex>; + let runInstanceMock: typemoq.IMock<TestRun>; + let mockTestItem: TestItem; + let mockParentItem: TestItem; + + setup(() => { + executionHandler = new TestExecutionHandler(); + testControllerMock = typemoq.Mock.ofType<TestController>(); + testItemIndexMock = typemoq.Mock.ofType<TestItemIndex>(); + runInstanceMock = typemoq.Mock.ofType<TestRun>(); + + mockTestItem = createMockTestItem('test1', 'Test 1'); + mockParentItem = createMockTestItem('parentTest', 'Parent Test'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processExecution', () => { + test('should process empty payload without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: {}, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process undefined result without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process multiple test results', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { test: 'test1', outcome: 'success', message: '', traceback: '' }, + test2: { test: 'test2', outcome: 'failure', message: 'Failed', traceback: 'traceback' }, + }, + error: '', + }; + + const mockTestItem2 = createMockTestItem('test2', 'Test 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + testItemIndexMock + .setup((x) => x.getTestItem('test2', testControllerMock.object)) + .returns(() => mockTestItem2); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockTestItem2, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestError', () => { + test('should create error message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error occurred', + traceback: 'line1\nline2\nline3', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Error occurred')); + assert.ok(messageText.includes('line1')); + assert.ok(messageText.includes('line2')); + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should set location when test item has range', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + assert.ok(capturedMessage!.location); + assert.strictEqual(capturedMessage!.location!.uri.fsPath, mockTestItem.uri!.fsPath); + }); + + test('should handle missing traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestFailure', () => { + test('should create failure message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'failure', + message: 'Assertion failed', + traceback: 'AssertionError\nline1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.failed(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Assertion failed')); + assert.ok(messageText.includes('AssertionError')); + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should handle passed-unexpected outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'passed-unexpected', + message: 'Unexpected pass', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestSuccess', () => { + test('should mark test as passed', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should handle expected-failure outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'expected-failure', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should not call passed when test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock.setup((x) => x.getTestItem('test1', testControllerMock.object)).returns(() => undefined); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); + + suite('handleTestSkipped', () => { + test('should mark test as skipped', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'skipped', + message: 'Test skipped', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.skipped(mockTestItem), typemoq.Times.once()); + }); + }); + + suite('handleSubtestFailure', () => { + test('should create child test item for subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Subtest failed', + traceback: 'traceback', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.failed === 1 && stats.passed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should update stats correctly for multiple subtests', () => { + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + + // First subtest: no existing stats + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + // Return different items based on call order + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => { + callCount++; + return callCount === 1 ? mockSubtest1 : mockSubtest2; + }); + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Second subtest: should have existing stats from first + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ failed: 1, passed: 0 })); + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify the first subtest set initial stats + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + }); + + test('should throw error when parent test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => undefined); + + assert.throws(() => { + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + }, /Parent test item not found/); + }); + }); + + suite('handleSubtestSuccess', () => { + test('should create passing subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + + test('should handle subtest with special characters in name', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest [subtest with spaces and [brackets]]': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest with spaces and [brackets]', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('[subtest with spaces and [brackets]]', 'Subtest'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + }); + + suite('Comprehensive Subtest Scenarios', () => { + test('should handle mixed passing and failing subtests in sequence', () => { + // Simulates unittest with subtests like: test_even with i=0,1,2,3,4,5 + const mockSubtest0 = createMockTestItem('(i=0)', '(i=0)'); + const mockSubtest1 = createMockTestItem('(i=1)', '(i=1)'); + const mockSubtest2 = createMockTestItem('(i=2)', '(i=2)'); + const mockSubtest3 = createMockTestItem('(i=3)', '(i=3)'); + const mockSubtest4 = createMockTestItem('(i=4)', '(i=4)'); + const mockSubtest5 = createMockTestItem('(i=5)', '(i=5)'); + + const subtestItems = [mockSubtest0, mockSubtest1, mockSubtest2, mockSubtest3, mockSubtest4, mockSubtest5]; + + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + + let subtestCallCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => subtestItems[subtestCallCount++]); + + // First subtest (i=0) - passes + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => undefined); + testItemIndexMock.setup((x) => x.setSubtestStats('test_even', typemoq.It.isAny())).returns(() => undefined); + + const payload0: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=0)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=0)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload0, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify first subtest created stats + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'test_even', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + // Second subtest (i=1) - fails + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 0 })); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=1)': { + test: 'test_even', + outcome: 'subtest-failure', + message: '1 is not even', + traceback: 'AssertionError', + subtest: '(i=1)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Third subtest (i=2) - passes + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 1 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=2)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=2)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify all subtests were started and had outcomes + runInstanceMock.verify((r) => r.started(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest1, typemoq.It.isAny()), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest2), typemoq.Times.once()); + }); + + test('should persist stats across multiple processExecution calls', () => { + // Test that stats persist in TestItemIndex across multiple processExecution calls + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => (callCount++ === 0 ? mockSubtest1 : mockSubtest2)); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + // First call - no existing stats + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Simulate stats being stored in TestItemIndex + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ passed: 1, failed: 0 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + // Second call - existing stats should be retrieved and updated + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify getSubtestStats was called to retrieve existing stats + testItemIndexMock.verify((x) => x.getSubtestStats('parentTest'), typemoq.Times.once()); + + // Verify both subtests were processed + runInstanceMock.verify((r) => r.passed(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest2, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should clear children only on first subtest when no existing stats', () => { + // When first subtest arrives, children should be cleared + // Subsequent subtests should NOT clear children + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtest1); + + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify setSubtestStats was called (which happens when creating new stats) + testItemIndexMock.verify((x) => x.setSubtestStats('parentTest', typemoq.It.isAny()), typemoq.Times.once()); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType<TestItemCollection>(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar/test.py'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/common/testItemIndex.unit.test.ts b/src/test/testing/testController/common/testItemIndex.unit.test.ts new file mode 100644 index 000000000000..6712d90ff667 --- /dev/null +++ b/src/test/testing/testController/common/testItemIndex.unit.test.ts @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, Range, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; + +suite('TestItemIndex', () => { + let testItemIndex: TestItemIndex; + let testControllerMock: typemoq.IMock<TestController>; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + let mockParentItem: TestItem; + + setup(() => { + testItemIndex = new TestItemIndex(); + testControllerMock = typemoq.Mock.ofType<TestController>(); + + // Create mock test items + mockTestItem1 = createMockTestItem('test1', 'Test 1'); + mockTestItem2 = createMockTestItem('test2', 'Test 2'); + mockParentItem = createMockTestItem('parent', 'Parent'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('registerTestItem', () => { + test('should store all three mappings correctly', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + + test('should overwrite existing mappings', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + testItemIndex.registerTestItem(runId, vsId, mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem2); + }); + + test('should handle different runId and vsId', () => { + const runId = 'test_file.py::TestClass::test_method'; + const vsId = 'different_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + }); + + suite('getTestItem', () => { + test('should return item on direct lookup when valid', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Register the item + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock the validation to return true + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, mockTestItem1); + assert.ok(isValidStub.calledOnce); + }); + + test('should remove stale item and try vsId fallback', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock validation to fail on first call (stale item) + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to not find the item + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should have removed the stale item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), undefined); + assert.strictEqual(result, undefined); + assert.ok(isValidStub.calledOnce); + }); + + test('should perform vsId search when direct lookup is stale', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Create test item with correct ID + const searchableTestItem = createMockTestItem(vsId, 'Test Example'); + + testItemIndex.registerTestItem(runId, vsId, searchableTestItem); + + // First validation fails (stale), need to search by vsId + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to find item by vsId + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + callback(searchableTestItem); + }) + .returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should recache the found item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), searchableTestItem); + assert.strictEqual(result, searchableTestItem); + }); + + test('should return undefined if not found anywhere', () => { + const runId = 'nonexistent'; + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, undefined); + }); + }); + + suite('getRunId and getVSId', () => { + test('getRunId should convert VS Code ID to Python run ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getRunId(vsId), runId); + }); + + test('getRunId should return undefined for unknown vsId', () => { + assert.strictEqual(testItemIndex.getRunId('unknown'), undefined); + }); + + test('getVSId should convert Python run ID to VS Code ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getVSId(runId), vsId); + }); + + test('getVSId should return undefined for unknown runId', () => { + assert.strictEqual(testItemIndex.getVSId('unknown'), undefined); + }); + }); + + suite('clear', () => { + test('should remove all mappings', () => { + testItemIndex.registerTestItem('runId1', 'vsId1', mockTestItem1); + testItemIndex.registerTestItem('runId2', 'vsId2', mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 2); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 2); + + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + + test('should handle clearing empty index', () => { + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('isTestItemValid', () => { + test('should return true for item with valid parent chain leading to controller', () => { + const childItem = createMockTestItem('child', 'Child'); + (childItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(mockParentItem.id)).returns(() => mockParentItem); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(childItem, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for orphaned item', () => { + const orphanedItem = createMockTestItem('orphaned', 'Orphaned'); + (orphanedItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(orphanedItem, testControllerMock.object); + + assert.strictEqual(result, false); + }); + + test('should return true for root item in controller', () => { + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(mockTestItem1.id)).returns(() => mockTestItem1); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for item not in controller and no parent', () => { + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, false); + }); + }); + + suite('cleanupStaleReferences', () => { + test('should remove items not in controller', () => { + const runId1 = 'test1'; + const runId2 = 'test2'; + const vsId1 = 'vs1'; + const vsId2 = 'vs2'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + testItemIndex.registerTestItem(runId2, vsId2, mockTestItem2); + + // Mock validation: first item invalid, second valid + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid'); + isValidStub.onFirstCall().returns(false); // mockTestItem1 is invalid + isValidStub.onSecondCall().returns(true); // mockTestItem2 is valid + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // First item should be removed + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), undefined); + + // Second item should remain + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId2), mockTestItem2); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId2), vsId2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId2), runId2); + }); + + test('should keep all valid items', () => { + const runId1 = 'test1'; + const vsId1 = 'vs1'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // Item should still be there + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), vsId1); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), runId1); + }); + + test('should handle empty index', () => { + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + }); + + test('should remove all items when all are invalid', () => { + testItemIndex.registerTestItem('test1', 'vs1', mockTestItem1); + testItemIndex.registerTestItem('test2', 'vs2', mockTestItem2); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('Backward compatibility getters', () => { + test('runIdToTestItemMap should return the internal map', () => { + const runId = 'test1'; + testItemIndex.registerTestItem(runId, 'vs1', mockTestItem1); + + const map = testItemIndex.runIdToTestItemMap; + + assert.strictEqual(map.get(runId), mockTestItem1); + }); + + test('runIdToVSidMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.runIdToVSidMap; + + assert.strictEqual(map.get(runId), vsId); + }); + + test('vsIdToRunIdMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.vsIdToRunIdMap; + + assert.strictEqual(map.get(vsId), runId); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType<TestItemCollection>(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/common/testProjectRegistry.unit.test.ts b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts new file mode 100644 index 000000000000..5d04930d0e88 --- /dev/null +++ b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { TestController, Uri } from 'vscode'; +import { IConfigurationService } from '../../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../../client/common/variables/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { TestProjectRegistry } from '../../../../client/testing/testController/common/testProjectRegistry'; +import * as envExtApiInternal from '../../../../client/envExt/api.internal'; +import { PythonProject, PythonEnvironment } from '../../../../client/envExt/types'; + +suite('TestProjectRegistry', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let configSettings: IConfigurationService; + let interpreterService: IInterpreterService; + let envVarsService: IEnvironmentVariablesProvider; + let registry: TestProjectRegistry; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create mock test controller + testController = ({ + items: { + get: sandbox.stub(), + add: sandbox.stub(), + delete: sandbox.stub(), + forEach: sandbox.stub(), + }, + createTestItem: sandbox.stub(), + dispose: sandbox.stub(), + } as unknown) as TestController; + + // Create mock config settings + configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + pytestEnabled: true, + unittestEnabled: false, + }, + }), + } as unknown) as IConfigurationService; + + // Create mock interpreter service + interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as unknown) as IInterpreterService; + + // Create mock env vars service + envVarsService = ({ + getEnvironmentVariables: sandbox.stub().resolves({}), + } as unknown) as IEnvironmentVariablesProvider; + + registry = new TestProjectRegistry(testController, configSettings, interpreterService, envVarsService); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('hasProjects', () => { + test('should return false for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.false; + }); + + test('should return true after projects are registered', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.true; + }); + }); + + suite('getProjectsArray', () => { + test('should return empty array for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').that.is.empty; + }); + + test('should return projects after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').with.length(1); + expect(result[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('discoverAndRegisterProjects', () => { + test('should create default project when env extension not available', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(projects[0].testProvider).to.equal('pytest'); + }); + + test('should use unittest when configured', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + (configSettings.getSettings as sinon.SinonStub).returns({ + testing: { + pytestEnabled: false, + unittestEnabled: true, + }, + }); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].testProvider).to.equal('unittest'); + }); + + test('should discover projects from Python Environments API', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectName).to.include('project1'); + expect(projects[0].pythonEnvironment).to.deep.equal(mockPythonEnv); + }); + + test('should filter projects to current workspace', async () => { + const workspaceUri = Uri.file('/workspace1'); + const projectInWorkspace = Uri.file('/workspace1/project1'); + const projectOutsideWorkspace = Uri.file('/workspace2/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: projectInWorkspace }, + { name: 'project2', uri: projectOutsideWorkspace }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(projectInWorkspace.fsPath); + }); + + test('should fallback to default project when no projects found', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [], + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + + test('should fallback to default project on API error', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').rejects(new Error('API error')); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('configureNestedProjectIgnores', () => { + test('should not set ignores when no nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + expect(projects[0].nestedProjectPathsToIgnore).to.be.undefined; + }); + + test('should configure ignore paths for nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const parentProjectUri = Uri.file('/workspace/parent'); + const childProjectUri = Uri.file(path.join('/workspace/parent', 'child')); + + const mockProjects: PythonProject[] = [ + { name: 'parent', uri: parentProjectUri }, + { name: 'child', uri: childProjectUri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + const parentProject = projects.find((p) => p.projectUri.fsPath === parentProjectUri.fsPath); + + expect(parentProject?.nestedProjectPathsToIgnore).to.include(childProjectUri.fsPath); + }); + + test('should not set child project as ignored for sibling projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const project1Uri = Uri.file('/workspace/project1'); + const project2Uri = Uri.file('/workspace/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: project1Uri }, + { name: 'project2', uri: project2Uri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + projects.forEach((project) => { + expect(project.nestedProjectPathsToIgnore).to.be.undefined; + }); + }); + }); + + suite('clearWorkspace', () => { + test('should remove all projects for a workspace', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + expect(registry.hasProjects(workspaceUri)).to.be.true; + + registry.clearWorkspace(workspaceUri); + + expect(registry.hasProjects(workspaceUri)).to.be.false; + expect(registry.getProjectsArray(workspaceUri)).to.be.empty; + }); + + test('should not affect other workspaces', async () => { + const workspace1Uri = Uri.file('/workspace1'); + const workspace2Uri = Uri.file('/workspace2'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspace1Uri); + await registry.discoverAndRegisterProjects(workspace2Uri); + + registry.clearWorkspace(workspace1Uri); + + expect(registry.hasProjects(workspace1Uri)).to.be.false; + expect(registry.hasProjects(workspace2Uri)).to.be.true; + }); + }); + + suite('getWorkspaceProjects', () => { + test('should return undefined for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.undefined; + }); + + test('should return map after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.instanceOf(Map); + expect(result?.size).to.equal(1); + }); + }); + + suite('ProjectAdapter properties', () => { + test('should create adapter with correct test infrastructure', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.projectName).to.be.a('string'); + expect(project.projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.workspaceUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.testProvider).to.equal('pytest'); + expect(project.discoveryAdapter).to.exist; + expect(project.executionAdapter).to.exist; + expect(project.resultResolver).to.exist; + expect(project.isDiscovering).to.be.false; + expect(project.isExecuting).to.be.false; + }); + + test('should include python environment details', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.pythonEnvironment).to.exist; + expect(project.pythonProject).to.exist; + expect(project.pythonProject.name).to.equal('myproject'); + }); + }); +}); diff --git a/src/test/testing/testController/controller.unit.test.ts b/src/test/testing/testController/controller.unit.test.ts new file mode 100644 index 000000000000..feb5f36fc797 --- /dev/null +++ b/src/test/testing/testController/controller.unit.test.ts @@ -0,0 +1,344 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { TestController, Uri } from 'vscode'; + +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import * as envExtApiInternal from '../../../client/envExt/api.internal'; +import * as projectUtils from '../../../client/testing/testController/common/projectUtils'; +import { PythonTestController } from '../../../client/testing/testController/controller'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; + +function createStubTestController(): TestController { + const disposable = { dispose: () => undefined }; + + const controller = ({ + items: { + forEach: sinon.stub(), + get: sinon.stub(), + add: sinon.stub(), + replace: sinon.stub(), + delete: sinon.stub(), + size: 0, + [Symbol.iterator]: sinon.stub(), + }, + createRunProfile: sinon.stub().returns(disposable), + createTestItem: sinon.stub(), + dispose: sinon.stub(), + resolveHandler: undefined, + refreshHandler: undefined, + } as unknown) as TestController; + + return controller; +} + +suite('PythonTestController', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + function createController(options?: { unittestEnabled?: boolean; interpreter?: any }): any { + const unittestEnabled = options?.unittestEnabled ?? false; + const interpreter = + options?.interpreter ?? + ({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + } as any); + + const workspaceService = ({ workspaceFolders: [] } as unknown) as any; + const configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + unittestEnabled, + autoTestDiscoverOnSaveEnabled: false, + }, + }), + } as unknown) as any; + + const pytest = ({} as unknown) as any; + const unittest = ({} as unknown) as any; + const disposables: any[] = []; + const interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as unknown) as any; + + const commandManager = ({ + registerCommand: sandbox.stub().returns({ dispose: () => undefined }), + } as unknown) as any; + const pythonExecFactory = ({} as unknown) as any; + const debugLauncher = ({} as unknown) as any; + const envVarsService = ({} as unknown) as any; + + return new PythonTestController( + workspaceService, + configSettings, + pytest, + unittest, + disposables, + interpreterService, + commandManager, + pythonExecFactory, + debugLauncher, + envVarsService, + ); + } + + suite('getTestProvider', () => { + test('returns unittest when enabled', () => { + const controller = createController({ unittestEnabled: true }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, UNITTEST_PROVIDER); + }); + + test('returns pytest when unittest not enabled', () => { + const controller = createController({ unittestEnabled: false }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, PYTEST_PROVIDER); + }); + }); + + suite('createDefaultProject (via TestProjectRegistry)', () => { + test('creates a single default project using active interpreter', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/myws'); + const interpreter = { + displayName: 'My Python', + path: '/opt/py/bin/python', + version: { raw: '3.12.1' }, + sysPrefix: '/opt/py', + }; + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + // Stub useEnvExtension to return false so createDefaultProject is called + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + assert.strictEqual(projects.length, 1); + assert.strictEqual(project.workspaceUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectName, 'myws'); + + assert.strictEqual(project.testProvider, PYTEST_PROVIDER); + assert.strictEqual(project.discoveryAdapter, fakeDiscoveryAdapter); + assert.strictEqual(project.executionAdapter, fakeExecutionAdapter); + + assert.strictEqual(project.pythonProject.uri.toString(), workspaceUri.toString()); + assert.strictEqual(project.pythonProject.name, 'myws'); + + assert.strictEqual(project.pythonEnvironment.displayName, 'My Python'); + assert.strictEqual(project.pythonEnvironment.version, '3.12.1'); + assert.strictEqual(project.pythonEnvironment.execInfo.run.executable, '/opt/py/bin/python'); + }); + }); + + suite('discoverWorkspaceProjects (via TestProjectRegistry)', () => { + test('respects useEnvExtension() == false and falls back to single default project', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/a'); + + const useEnvExtensionStub = sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + const getEnvExtApiStub = sandbox.stub(envExtApiInternal, 'getEnvExtApi'); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + assert.strictEqual(useEnvExtensionStub.called, true); + assert.strictEqual(getEnvExtApiStub.notCalled, true); + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + + test('filters Python projects to workspace and creates adapters for each', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + const pythonProjects = [ + { name: 'p1', uri: vscode.Uri.file('/workspace/root/p1') }, + { name: 'p2', uri: vscode.Uri.file('/workspace/root/nested/p2') }, + { name: 'other', uri: vscode.Uri.file('/other/root/p3') }, + ]; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => pythonProjects, + getEnvironment: sandbox.stub().resolves({ + name: 'env', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: vscode.Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'test', managerId: 'test' }, + }), + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(null), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should only create adapters for the 2 projects in the workspace (not 'other') + assert.strictEqual(projects.length, 2); + const projectUris = projects.map((p: { projectUri: { fsPath: string } }) => p.projectUri.fsPath); + const expectedInWorkspace = [ + vscode.Uri.file('/workspace/root/p1').fsPath, + vscode.Uri.file('/workspace/root/nested/p2').fsPath, + ]; + const expectedOutOfWorkspace = vscode.Uri.file('/other/root/p3').fsPath; + + expectedInWorkspace.forEach((expectedPath) => { + assert.ok(projectUris.includes(expectedPath)); + }); + assert.ok(!projectUris.includes(expectedOutOfWorkspace)); + }); + + test('falls back to default project when no projects are in the workspace', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [{ name: 'other', uri: vscode.Uri.file('/other/root/p3') }], + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreter = { + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }; + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should fall back to default project since no projects are in the workspace + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + }); +}); diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts new file mode 100644 index 000000000000..7f2f5e23bfc3 --- /dev/null +++ b/src/test/testing/testController/payloadTestCases.ts @@ -0,0 +1,171 @@ +export interface DataWithPayloadChunks { + payloadArray: string[]; + data: string; +} + +const SINGLE_UNITTEST_SUBTEST = { + cwd: '/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace', + status: 'success', + result: { + 'test_parameterized_subtest.NumbersTest.test_even (i=0)': { + test: 'test_parameterized_subtest.NumbersTest.test_even', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'test_parameterized_subtest.NumbersTest.test_even (i=0)', + }, + }, +}; + +export const SINGLE_PYTEST_PAYLOAD = { + cwd: 'path/to', + status: 'success', + result: { + 'path/to/file.py::test_funct': { + test: 'path/to/file.py::test_funct', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'path/to/file.py::test_funct', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD_TWO = { + cwd: 'path/to/second', + status: 'success', + result: { + 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]': { + test: 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]', + outcome: 'success', + message: 'None', + traceback: null, + }, + }, +}; + +function splitIntoRandomSubstrings(payload: string): string[] { + // split payload at random + const splitPayload = []; + const n = payload.length; + let remaining = n; + while (remaining > 0) { + // Randomly split what remains of the string + const randomSize = Math.floor(Math.random() * remaining) + 1; + splitPayload.push(payload.slice(n - remaining, n - remaining + randomSize)); + + remaining -= randomSize; + } + return splitPayload; +} + +export function createPayload(uuid: string, data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json +Request-uuid: ${uuid} + +${JSON.stringify(data)}`; +} + +export function createPayload2(data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json + +${JSON.stringify(data)}`; +} + +export function PAYLOAD_SINGLE_CHUNK(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + + return { + payloadArray: [payload], + data: JSON.stringify(SINGLE_UNITTEST_SUBTEST.result), + }; +} + +// more than one payload (item with header) per chunk sent +// payload has 3 SINGLE_UNITTEST_SUBTEST +export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + let payload = ''; + let result = ''; + for (let i = 0; i < 3; i = i + 1) { + payload += createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + result += JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + } + return { + payloadArray: [payload], + data: result, + }; +} + +// more than one payload, split so the first one is only 'Content-Length' to confirm headers +// with null values are ignored +export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + const payloadArray: string[] = []; + const result = JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + + const val = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + const firstSpaceIndex = val.indexOf(' '); + const payload1 = val.substring(0, firstSpaceIndex); + const payload2 = val.substring(firstSpaceIndex); + payloadArray.push(payload1); + payloadArray.push(payload2); + return { + payloadArray, + data: result, + }; +} + +// single payload divided by an arbitrary character and split across payloads +export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +// here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk +export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD).concat(createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO)); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( + JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), + ); + + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +export function PAYLOAD_SPLIT_MULTI_CHUNK_RAN_ORDER_ARRAY(uuid: string): Array<string> { + return [ + `Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=0)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=0)"}}} + +Content-Length: 411 +Content-Type: application/json +Request-uuid: 9${uuid} + +{"cwd": "/home/runner/work/vscode-`, + `python/vscode-python/path with`, + ` spaces/src" + +Content-Length: 959 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-failure", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=1)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-failure", "message": "(<class 'AssertionError'>, AssertionError('1 != 0'), <traceback object at 0x7fd86fc47580>)", "traceback": " File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n yield\n File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 538, in subTest\n yield\n File \"/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py\", line 16, in test_even\n self.assertEqual(i % 2, 0)\nAssertionError: 1 != 0\n", "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=1)"}}} +Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=2)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=2)"}}}`, + ]; +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts new file mode 100644 index 000000000000..ec155ee3107d --- /dev/null +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -0,0 +1,414 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { Uri, CancellationTokenSource } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import * as fs from 'fs'; +import * as sinon from 'sinon'; +import { IConfigurationService } from '../../../../client/common/types'; +import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + SpawnOptions, + Output, +} from '../../../../client/common/process/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; +import * as util from '../../../../client/testing/testController/common/utils'; +import * as extapi from '../../../../client/envExt/api.internal'; + +suite('pytest test discovery adapter', () => { + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let adapter: PytestTestDiscoveryAdapter; + let execService: typeMoq.IMock<IPythonExecutionService>; + let deferred: Deferred<void>; + let expectedPath: string; + let uri: Uri; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let expectedExtraVariables: Record<string, string>; + let mockProc: MockChildProcess; + let deferred2: Deferred<void>; + let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + const mockExtensionRootDir = typeMoq.Mock.ofType<string>(); + mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); + + utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + + // constants + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + const relativePathToPytest = 'python_files'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + expectedExtraVariables = { + PYTHONPATH: fullPluginPath, + TEST_RUN_PIPE: 'discoveryResultPipe-mockName', + }; + + // set up config service + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + deferred2 = createDeferred(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + + cancellationTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancellationTokenSource.dispose(); + }); + test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is<SpawnOptions>((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery correctly pulls pytest args from config service settings', async () => { + // set up a config service with different pytest args + const expectedPathNew = path.join('other', 'path'); + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, + }), + } as unknown) as IConfigurationService; + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPathNew}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPathNew); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery adds cwd to pytest args when path is symlink', async () => { + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => true, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPath, + }, + }), + } as unknown) as IConfigurationService; + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPath}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery adds cwd to pytest args when path parent is symlink', async () => { + let counter = 0; + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => { + counter = counter + 1; + return counter > 2; + }, + } as fs.Stats), + ); + + sinon.stub(fs.promises, 'realpath').callsFake(async () => 'diff value'); + + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPath, + }, + }), + } as unknown) as IConfigurationService; + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPath}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); + + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable<Output<string>>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); + }); +}); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts new file mode 100644 index 000000000000..40c701b22641 --- /dev/null +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -0,0 +1,535 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { TestRun, Uri, TestRunProfileKind, DebugSessionOptions } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import { IConfigurationService } from '../../../../client/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { createMockProjectAdapter } from '../testMocks'; + +suite('pytest test execution adapter', () => { + let useEnvExtensionStub: sinon.SinonStub; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock<IPythonExecutionService>; + let deferred: Deferred<void>; + let deferred4: Deferred<void>; + let debugLauncher: typeMoq.IMock<ITestDebugLauncher>; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipeStub: sinon.SinonStub; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType<ITestDebugLauncher>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + + utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + }); + teardown(() => { + sinon.restore(); + }); + test('WriteTestIdsFile called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve({ + name: 'mockName', + dispose: () => { + /* no-op */ + }, + }); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + const testIds = ['test1id', 'test2id']; + + adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); + + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsWriteTestIdsFileStub, testIds); + }); + test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const rootDirArg = `--rootdir=${myTestPath}`; + const expectedArgs = [pathToPythonScript, rootDirArg]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.env?.COVERAGE_ENABLED, undefined); // coverage not enabled + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const expectedArgs = [pathToPythonScript, `--rootdir=${newCwd}`]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.deepEqual(launchOptions.args, [`--rootdir=${myTestPath}`, '--capture=no']); + assert.equal(launchOptions.testProvider, 'pytest'); + assert.equal(launchOptions.pytestPort, 'runResultPipe-mockName'); + assert.strictEqual(launchOptions.runTestIdsPort, 'testIdPipe-mockName'); + assert.notEqual(launchOptions.token, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.is<DebugSessionOptions>((sessionOptions) => { + assert.equal(sessionOptions.testRun, testRun.object); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('pytest execution with coverage turns on correctly', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const rootDirArg = `--rootdir=${myTestPath}`; + const expectedArgs = [pathToPythonScript, rootDirArg]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.COVERAGE_ENABLED, 'True'); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + // ===== PROJECT-BASED EXECUTION TESTS ===== + + suite('project-based execution', () => { + test('should set PROJECT_ROOT_PATH env var when project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, + undefined, + mockProject, + ); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, projectPath); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should pass debugSessionName in LaunchOptions for debug mode with project', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set PROJECT_ROOT_PATH when no project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, undefined); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set project in LaunchOptions when no project provided', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + assert.equal(launchOptions.project, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + }); +}); diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts new file mode 100644 index 000000000000..e4b350a20750 --- /dev/null +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -0,0 +1,613 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, Uri, TestItem, CancellationToken, TestRun, TestItemCollection, Range } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { TestProvider } from '../../../client/testing/types'; +import { + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import * as util from '../../../client/testing/testController/common/utils'; +import { traceLog } from '../../../client/logging'; + +suite('Result Resolver tests', () => { + suite('Test discovery', () => { + let resultResolver: ResultResolver.PythonResultResolver; + let testController: TestController; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let defaultErrorMessage: string; + let blankTestItem: TestItem; + let cancelationToken: CancellationToken; + + setup(() => { + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + + dispose: () => { + // empty + }, + } as unknown) as TestController; + defaultErrorMessage = 'pytest test discovery error (see Output > Python)'; + blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + teardown(() => { + sinon.restore(); + }); + + test('resolveDiscovery calls populate test tree correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of populateTestTree is (testController: TestController, testTreeData: DiscoveredTestNode, testRoot: TestItem | undefined, resultResolver: ITestResultResolver, token?: CancellationToken) + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + sinon.match.has('runIdToTestItem'), // inline object with maps + cancelationToken, // token + ); + }); + test('resolveDiscovery should create error node on error with correct params and no root node with tests in payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of buildErrorNodeOptions is (uri: Uri, message: string, testType: string) + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // header of createErrorTestItem is (options: ErrorTestItemOptions, testController: TestController, uri: Uri) + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + }); + test('resolveDiscovery should create error and root node when error and tests exist on payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + // also calls populateTestTree with the discovery test results + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + sinon.match.has('runIdToTestItem'), // inline object with maps + cancelationToken, // token + ); + }); + test('resolveDiscovery should create error and not clear test items to allow for error tolerant discovery', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const errorPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + }; + const regPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + sinon.stub(util, 'populateTestTree').returns(); + // add spies to insure these aren't called + const deleteSpy = sinon.spy(testController.items, 'delete'); + const replaceSpy = sinon.spy(testController.items, 'replace'); + // call resolve discovery + resultResolver.resolveDiscovery(regPayload, cancelationToken); + resultResolver.resolveDiscovery(errorPayload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + if (!deleteSpy.calledOnce) { + throw new Error("The delete method was called, but it shouldn't have been."); + } + if (replaceSpy.called) { + throw new Error("The replace method was called, but it shouldn't have been."); + } + }); + }); + suite('Test execution result resolver', () => { + let resultResolver: ResultResolver.PythonResultResolver; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + let runInstance: typemoq.IMock<TestRun>; + let testControllerMock: typemoq.IMock<TestController>; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + + setup(() => { + // create mock test items + mockTestItem1 = createMockTestItem('mockTestItem1'); + mockTestItem2 = createMockTestItem('mockTestItem2'); + + // create mock testItems to pass into a iterable + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + // create mock testItemCollection + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + + // create mock testController + testControllerMock = typemoq.Mock.ofType<TestController>(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + + // define functions within runInstance + runInstance = typemoq.Mock.ofType<TestRun>(); + runInstance.setup((r) => r.name).returns(() => 'name'); + runInstance.setup((r) => r.token).returns(() => cancelationToken); + runInstance.setup((r) => r.isPersisted).returns(() => true); + runInstance + .setup((r) => r.enqueued(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('enqueue'); + return undefined; + }); + runInstance + .setup((r) => r.started(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('start'); + }); + + // mock getTestCaseNodes to just return the given testNode added + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => [testNode]); + }); + teardown(() => { + sinon.restore(); + }); + test('resolveExecution create correct subtest item for unittest', async () => { + // test specific constants used expected values + sinon.stub(testItemUtilities, 'clearAllChildren').callsFake(() => undefined); + testProvider = 'unittest'; + workspaceUri = Uri.file('/foo/bar'); + + // Create parent test item with correct ID + const mockParentItem = createMockTestItem('parentTest'); + + // Update testControllerMock to include parent item in its collection + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ['parentTest', mockParentItem], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testItemCollectionMock.setup((x) => x.get('parentTest')).returns(() => mockParentItem); + + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + const subtestName = 'parentTest [subTest with spaces and [brackets]]'; + const mockSubtestItem = createMockTestItem(subtestName); + + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + // creates a mock test item with a space which will be used to split the runId + resultResolver.runIdToVSid.set(subtestName, subtestName); + // Register parent test in testItemIndex so it can be found by getTestItem + resultResolver.runIdToVSid.set('parentTest', 'parentTest'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('parentTest', mockParentItem); + resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); + + let generatedId: string | undefined; + let generatedUri: Uri | undefined; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .callback((id: string) => { + generatedId = id; + generatedUri = workspaceUri; + traceLog('createTestItem function called with id:', id); + }) + .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + 'parentTest [subTest with spaces and [brackets]]': { + test: 'parentTest', + outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: subtestName, + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + assert.ok(generatedId); + assert.strictEqual(generatedUri, workspaceUri); + assert.strictEqual(generatedId, '[subTest with spaces and [brackets]]'); + }); + test('resolveExecution handles failed tests correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'failure', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles skipped correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'skipped', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly as test outcome', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'error', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles success correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + + const errorPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: 'error', + }; + + resultResolver.resolveExecution(errorPayload, runInstance.object); + + // verify that none of these functions are called + + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); +}); + +function createMockTestItem(id: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType<TestItemCollection>(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + mockChildren.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts new file mode 100644 index 000000000000..cdf0d00c5dc4 --- /dev/null +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -0,0 +1,234 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as extapi from '../../../client/envExt/api.internal'; +import { noop } from '../../core'; + +const adapters: Array<string> = ['pytest', 'unittest']; + +suite('Execution Flow Run Adapters', () => { + // define suit level variables + let configService: IConfigurationService; + let execFactoryStub = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let execServiceStub: typeMoq.IMock<IPythonExecutionService>; + // let deferred: Deferred<void>; + let debugLauncher: typeMoq.IMock<ITestDebugLauncher>; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipe: sinon.SinonStub; + let serverDisposeStub: sinon.SinonStub; + + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + const proc = typeMoq.Mock.ofType<MockChildProcess>(); + proc.setup((p) => p.on).returns(() => noop as any); + proc.setup((p) => p.stdout).returns(() => null); + proc.setup((p) => p.stderr).returns(() => null); + mockProc = proc.object; + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + // general vars + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up execService and execFactory, all mocked + execServiceStub = typeMoq.Mock.ofType<IPythonExecutionService>(); + execFactoryStub = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + + // mocked utility functions that handle pipe related functions + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + utilsStartRunResultNamedPipe = sinon.stub(util, 'startRunResultNamedPipe'); + serverDisposeStub = sinon.stub(); + + // debug specific mocks + debugLauncher = typeMoq.Mock.ofType<ITestDebugLauncher>(); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + }); + teardown(() => { + sinon.restore(); + }); + adapters.forEach((adapter) => { + test(`Adapter ${adapter}: cancelation token called mid-run resolves correctly`, async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType<TestRun>(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred<void> | undefined; + + // // mock exec service and exec factory + execServiceStub + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc as any, + out: typeMoq.Mock.ofType<Observable<Output<string>>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactoryStub + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceStub.object)); + execFactoryStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // test ids named pipe mocking + const deferredStartTestIdsNamedPipe = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => { + deferredStartTestIdsNamedPipe.resolve(); + return Promise.resolve('named-pipe'); + }); + + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, token) => { + deferredTillServerCloseTester = deferredTillServerClose; + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); + }); + + return Promise.resolve('named-pipes-socket-name'); + }); + serverDisposeStub.callsFake(() => { + console.log('server disposed'); + if (deferredTillServerCloseTester) { + deferredTillServerCloseTester.resolve(); + } else { + console.log('deferredTillServerCloseTester is undefined'); + throw new Error( + 'deferredTillServerCloseTester is undefined, should be defined from startRunResultNamedPipe', + ); + } + }); + + // define adapter and run tests + const testAdapter = createAdapter(adapter, configService); + await testAdapter.runTests( + Uri.file(myTestPath), + [], + TestRunProfileKind.Run, + testRunMock.object, + execFactoryStub.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartTestIdsNamedPipe.promise; + }); + test(`Adapter ${adapter}: token called mid-debug resolves correctly`, async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType<TestRun>(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred<void> | undefined; + + // // mock exec service and exec factory + execServiceStub + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc as any, + out: typeMoq.Mock.ofType<Observable<Output<string>>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactoryStub + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceStub.object)); + execFactoryStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // test ids named pipe mocking + const deferredStartTestIdsNamedPipe = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => { + deferredStartTestIdsNamedPipe.resolve(); + return Promise.resolve('named-pipe'); + }); + + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { + deferredTillServerCloseTester = deferredTillServerClose; + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); + }); + return Promise.resolve('named-pipes-socket-name'); + }); + serverDisposeStub.callsFake(() => { + console.log('server disposed'); + if (deferredTillServerCloseTester) { + deferredTillServerCloseTester.resolve(); + } else { + console.log('deferredTillServerCloseTester is undefined'); + throw new Error( + 'deferredTillServerCloseTester is undefined, should be defined from startRunResultNamedPipe', + ); + } + }); + + // debugLauncher mocked + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .callback((_options, callback) => { + if (callback) { + callback(); + } + }) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + // define adapter and run tests + const testAdapter = createAdapter(adapter, configService); + await testAdapter.runTests( + Uri.file(myTestPath), + [], + TestRunProfileKind.Debug, + testRunMock.object, + execFactoryStub.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartTestIdsNamedPipe.promise; + }); + }); +}); + +// Helper function to create an adapter based on the specified type +function createAdapter( + adapterType: string, + configService: IConfigurationService, +): PytestTestExecutionAdapter | UnittestTestExecutionAdapter { + if (adapterType === 'pytest') return new PytestTestExecutionAdapter(configService); + if (adapterType === 'unittest') return new UnittestTestExecutionAdapter(configService); + throw Error('un-compatible adapter type'); +} diff --git a/src/test/testing/testController/testMocks.ts b/src/test/testing/testController/testMocks.ts new file mode 100644 index 000000000000..eb37d492f1d9 --- /dev/null +++ b/src/test/testing/testController/testMocks.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Centralized mock utilities for testing testController components. + * Re-use these helpers across multiple test files for consistency. + */ + +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ProjectAdapter } from '../../../client/testing/testController/common/projectAdapter'; +import { ProjectExecutionDependencies } from '../../../client/testing/testController/common/projectTestExecution'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; +import { ITestExecutionAdapter, ITestResultResolver } from '../../../client/testing/testController/common/types'; + +/** + * Creates a mock TestItem with configurable properties. + * @param id - The unique ID of the test item + * @param uriPath - The file path for the test item's URI + * @param children - Optional array of child test items + */ +export function createMockTestItem(id: string, uriPath: string, children?: TestItem[]): TestItem { + const childMap = new Map<string, TestItem>(); + children?.forEach((c) => childMap.set(c.id, c)); + + const mockChildren: TestItemCollection = { + size: childMap.size, + forEach: (callback: (item: TestItem, collection: TestItemCollection) => void) => { + childMap.forEach((item) => callback(item, mockChildren)); + }, + get: (itemId: string) => childMap.get(itemId), + add: () => {}, + delete: () => {}, + replace: () => {}, + [Symbol.iterator]: function* () { + for (const [key, value] of childMap) { + yield [key, value] as [string, TestItem]; + } + }, + } as TestItemCollection; + + return ({ + id, + uri: Uri.file(uriPath), + children: mockChildren, + label: id, + canResolveChildren: false, + busy: false, + tags: [], + range: undefined, + error: undefined, + parent: undefined, + } as unknown) as TestItem; +} + +/** + * Creates a mock TestItem without a URI. + * Useful for testing edge cases where test items have no associated file. + * @param id - The unique ID of the test item + */ +export function createMockTestItemWithoutUri(id: string): TestItem { + return ({ + id, + uri: undefined, + children: ({ size: 0, forEach: () => {} } as unknown) as TestItemCollection, + label: id, + } as unknown) as TestItem; +} + +export interface MockProjectAdapterConfig { + projectPath: string; + projectName: string; + pythonPath?: string; + testProvider?: 'pytest' | 'unittest'; +} + +export type MockProjectAdapter = ProjectAdapter & { executionAdapterStub: sinon.SinonStub }; + +/** + * Creates a mock ProjectAdapter for testing project-based test execution. + * @param config - Configuration object with project details + * @returns A mock ProjectAdapter with an exposed executionAdapterStub for verification + */ +export function createMockProjectAdapter(config: MockProjectAdapterConfig): MockProjectAdapter { + const runTestsStub = sinon.stub().resolves(); + const executionAdapter: ITestExecutionAdapter = ({ + runTests: runTestsStub, + } as unknown) as ITestExecutionAdapter; + + const resultResolverMock: ITestResultResolver = ({ + vsIdToRunId: new Map<string, string>(), + runIdToVSid: new Map<string, string>(), + runIdToTestItem: new Map<string, TestItem>(), + detailedCoverageMap: new Map(), + resolveDiscovery: () => Promise.resolve(), + resolveExecution: () => {}, + } as unknown) as ITestResultResolver; + + const adapter = ({ + projectUri: Uri.file(config.projectPath), + projectName: config.projectName, + workspaceUri: Uri.file(config.projectPath), + testProvider: config.testProvider ?? 'pytest', + pythonEnvironment: config.pythonPath + ? { + execInfo: { run: { executable: config.pythonPath } }, + } + : undefined, + pythonProject: { + name: config.projectName, + uri: Uri.file(config.projectPath), + }, + executionAdapter, + discoveryAdapter: {} as any, + resultResolver: resultResolverMock, + isDiscovering: false, + isExecuting: false, + // Expose the stub for testing + executionAdapterStub: runTestsStub, + } as unknown) as MockProjectAdapter; + + return adapter; +} + +/** + * Creates mock dependencies for project test execution. + * @returns An object containing mocked ProjectExecutionDependencies + */ +export function createMockDependencies(): ProjectExecutionDependencies { + return { + projectRegistry: typemoq.Mock.ofType<TestProjectRegistry>().object, + pythonExecFactory: typemoq.Mock.ofType<IPythonExecutionFactory>().object, + debugLauncher: typemoq.Mock.ofType<ITestDebugLauncher>().object, + }; +} + +/** + * Creates a mock TestRun with common setup methods. + * @returns A TypeMoq mock of TestRun + */ +export function createMockTestRun(): typemoq.IMock<TestRun> { + const runMock = typemoq.Mock.ofType<TestRun>(); + runMock.setup((r) => r.started(typemoq.It.isAny())); + runMock.setup((r) => r.passed(typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.skipped(typemoq.It.isAny())); + runMock.setup((r) => r.end()); + return runMock; +} diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts new file mode 100644 index 000000000000..031f30afba8a --- /dev/null +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as assert from 'assert'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import * as fs from 'fs'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { Observable } from 'rxjs'; +import * as sinon from 'sinon'; +import { IConfigurationService } from '../../../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; + +suite('Unittest test discovery adapter', () => { + let configService: IConfigurationService; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock<IPythonExecutionService>; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let deferred: Deferred<void>; + let expectedExtraVariables: Record<string, string>; + let expectedPath: string; + let uri: Uri; + let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + expectedPath = path.join('/', 'new', 'cwd'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + console.log('execObservable is returning'); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + deferred = createDeferred(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // constants + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + expectedExtraVariables = { + TEST_RUN_PIPE: 'discoveryResultPipe-mockName', + }; + + utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + cancellationTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancellationTokenSource.dispose(); + }); + + test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('DiscoverTests should respect settings.testings.cwd when present', async () => { + const expectedNewPath = path.join('/', 'new', 'cwd'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: expectedNewPath.toString() }, + }), + } as unknown) as IConfigurationService; + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedNewPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + const adapter = new UnittestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); + + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable<Output<string>>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + const adapter = new UnittestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); + }); + + test('DiscoverTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; + + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object, undefined, undefined, mockProject); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + + test('DiscoverTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); +}); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts new file mode 100644 index 000000000000..8a86e9228567 --- /dev/null +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -0,0 +1,581 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import { IConfigurationService } from '../../../../client/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; +import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; +import { createMockProjectAdapter } from '../testMocks'; + +suite('Unittest test execution adapter', () => { + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let adapter: UnittestTestExecutionAdapter; + let execService: typeMoq.IMock<IPythonExecutionService>; + let deferred: Deferred<void>; + let deferred4: Deferred<void>; + let debugLauncher: typeMoq.IMock<ITestDebugLauncher>; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['.'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType<ITestDebugLauncher>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + + utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + }); + teardown(() => { + sinon.restore(); + }); + test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve({ + name: 'mockName', + dispose: () => { + /* no-op */ + }, + }); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + const testIds = ['test1id', 'test2id']; + + adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); + + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsWriteTestIdsFileStub, testIds); + }); + test('unittest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + const expectedExtraVariables = { + PYTHONPATH: myTestPath, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.env?.COVERAGE_ENABLED, undefined); // coverage not enabled + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('unittest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + const expectedExtraVariables = { + PYTHONPATH: newCwd, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Debug launched correctly for unittest', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.equal(launchOptions.testProvider, 'unittest'); + assert.equal(launchOptions.pytestPort, 'runResultPipe-mockName'); + assert.strictEqual(launchOptions.runTestIdsPort, 'testIdPipe-mockName'); + assert.notEqual(launchOptions.token, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.is<DebugSessionOptions>((sessionOptions) => { + assert.equal(sessionOptions.testRun, testRun.object); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('unittest execution with coverage turned on correctly', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.COVERAGE_ENABLED, uri.fsPath); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('RunTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, // debugLauncher + undefined, // interpreter + mockProject, + ); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('RunTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('Debug mode with project should pass project.pythonProject to debug launcher', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('useEnvExtension mode with project should use project pythonEnvironment', async () => { + // Enable the useEnvExtension path + useEnvExtensionStub.returns(true); + + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + // Store the deferredTillServerClose so we can resolve it + let serverCloseDeferred: Deferred<void> | undefined; + utilsStartRunResultNamedPipeStub.callsFake((_callback: unknown, deferred: Deferred<void>, _token: unknown) => { + serverCloseDeferred = deferred; + return Promise.resolve('runResultPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + // Stub runInBackground to capture which environment was used + const runInBackgroundStub = sinon.stub(extapi, 'runInBackground'); + const exitCallbacks: ((code: number, signal: string | null) => void)[] = []; + // Promise that resolves when the production code registers its onExit handler + const onExitRegistered = createDeferred<void>(); + const mockProc2 = { + stdout: { on: sinon.stub() }, + stderr: { on: sinon.stub() }, + onExit: (cb: (code: number, signal: string | null) => void) => { + exitCallbacks.push(cb); + onExitRegistered.resolve(); + }, + kill: sinon.stub(), + }; + runInBackgroundStub.callsFake(() => Promise.resolve(mockProc2 as any)); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + const runPromise = adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + // Wait for production code to register its onExit handler + await onExitRegistered.promise; + + // Simulate process exit to complete the test + exitCallbacks.forEach((cb) => cb(0, null)); + + // Resolve the server close deferred to allow the runTests to complete + serverCloseDeferred?.resolve(); + + await runPromise; + + // Verify runInBackground was called with the project's Python environment + sinon.assert.calledOnce(runInBackgroundStub); + const envArg = runInBackgroundStub.firstCall.args[0]; + // The environment should be the project's pythonEnvironment + assert.ok(envArg, 'runInBackground should be called with an environment'); + assert.equal(envArg.execInfo?.run?.executable, '/custom/python/path'); + }); +}); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts new file mode 100644 index 000000000000..3cba6fb697a5 --- /dev/null +++ b/src/test/testing/testController/utils.unit.test.ts @@ -0,0 +1,754 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { CancellationToken, TestController, TestItem, Uri, Range, Position } from 'vscode'; +import { writeTestIdsFile, populateTestTree } from '../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { + DiscoveredTestNode, + DiscoveredTestItem, + ITestResultResolver, +} from '../../../client/testing/testController/common/types'; +import { RunTestTag, DebugTestTag } from '../../../client/testing/testController/common/testItemUtilities'; + +suite('writeTestIdsFile tests', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should write test IDs to a temporary file', async () => { + const testIds = ['test1', 'test2', 'test3']; + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); + + // Set up XDG_RUNTIME_DIR + process.env = { + ...process.env, + XDG_RUNTIME_DIR: '/xdg/runtime/dir', + }; + + await writeTestIdsFile(testIds); + + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); + + test('should handle error when accessing temp directory', async () => { + const testIds = ['test1', 'test2', 'test3']; + const error = new Error('Access error'); + const accessStub = sandbox.stub(fs.promises, 'access').rejects(error); + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); + const mkdirStub = sandbox.stub(fs.promises, 'mkdir').resolves(); + + const result = await writeTestIdsFile(testIds); + + const tempFileFolder = path.join(EXTENSION_ROOT_DIR, '.temp'); + + assert.ok(result.startsWith(tempFileFolder)); + + assert.ok(accessStub.called); + assert.ok(mkdirStub.called); + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); +}); + +suite('getTempDir tests', () => { + let sandbox: sinon.SinonSandbox; + let originalPlatform: NodeJS.Platform; + let originalEnv: NodeJS.ProcessEnv; + + setup(() => { + sandbox = sinon.createSandbox(); + originalPlatform = process.platform; + originalEnv = process.env; + }); + + teardown(() => { + sandbox.restore(); + Object.defineProperty(process, 'platform', { value: originalPlatform }); + process.env = originalEnv; + }); + + test('should use XDG_RUNTIME_DIR on non-Windows if available', async () => { + if (process.platform === 'win32') { + return; + } + // Force platform to be Linux + Object.defineProperty(process, 'platform', { value: 'linux' }); + + // Set up XDG_RUNTIME_DIR + process.env = { ...process.env, XDG_RUNTIME_DIR: '/xdg/runtime/dir' }; + + const testIds = ['test1', 'test2', 'test3']; + sandbox.stub(fs.promises, 'access').resolves(); + sandbox.stub(fs.promises, 'writeFile').resolves(); + + // This will use getTempDir internally + const result = await writeTestIdsFile(testIds); + + assert.ok(result.startsWith('/xdg/runtime/dir')); + }); +}); + +suite('populateTestTree tests', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let resultResolver: ITestResultResolver; + let cancelationToken: CancellationToken; + let createTestItemStub: sinon.SinonStub; + let itemsAddStub: sinon.SinonStub; + let itemsGetStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create stubs for TestController methods + createTestItemStub = sandbox.stub(); + itemsAddStub = sandbox.stub(); + itemsGetStub = sandbox.stub(); + + // Create mock TestController + testController = { + createTestItem: createTestItemStub, + items: { + add: itemsAddStub, + get: itemsGetStub, + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + // Create mock result resolver + resultResolver = { + runIdToTestItem: new Map(), + runIdToVSid: new Map(), + vsIdToRunId: new Map(), + detailedCoverageMap: new Map(), + resolveDiscovery: sandbox.stub(), + resolveExecution: sandbox.stub(), + _resolveDiscovery: sandbox.stub(), + _resolveExecution: sandbox.stub(), + _resolveCoverage: sandbox.stub(), + }; + + // Mock cancellation token + cancelationToken = { + isCancellationRequested: false, + onCancellationRequested: sandbox.stub(), + } as any; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should create a root node if testRoot is undefined', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const mockRootItem: TestItem = { + id: '/test/path/root', + label: 'RootTest', + uri: Uri.file('/test/path/root'), + canResolveChildren: true, + tags: [RunTestTag, DebugTestTag], + children: { + add: sandbox.stub(), + get: sandbox.stub(), + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + createTestItemStub.returns(mockRootItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnce); + // Check the args manually - function uses testTreeData.path as id + const call = createTestItemStub.firstCall; + assert.strictEqual(call.args[0], '/test/path/root'); + assert.strictEqual(call.args[1], 'RootTest'); + // Don't check Uri.file since it's complex to compare + assert.ok(itemsAddStub.calledOnceWith(mockRootItem)); + assert.strictEqual(mockRootItem.canResolveChildren, true); + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should recursively add children as TestItems', () => { + // Arrange + // Tree structure: + // RootWorkspaceFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootWorkspaceFolder', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + label: 'test_example', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + assert.strictEqual(mockTestItem.canResolveChildren, false); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should create TestItem with correct range when lineno is provided', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 5, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + const expectedRange = new Range(new Position(4, 0), new Position(5, 0)); + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should handle lineno = 0 correctly', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: '0', + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert- if lineno is '0', range should be defined but at the top + const expectedRange = new Range(new Position(0, 0), new Position(0, 0)); + + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should update resultResolver mappings correctly for test items', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.strictEqual(resultResolver.runIdToTestItem.get('run-id-123'), mockTestItem); + assert.strictEqual(resultResolver.runIdToVSid.get('run-id-123'), 'test-id'); + assert.strictEqual(resultResolver.vsIdToRunId.get('test-id'), 'run-id-123'); + }); + + test('should create nodes for non-leaf items and recurse', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── nested_test (test) + const nestedTestItem: DiscoveredTestItem = { + path: '/test/path/nested_test.py', + name: 'nested_test', + type_: 'test', + id_: 'nested-test-id', + lineno: 5, + runID: 'nested-run-id', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [nestedTestItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const rootChildrenGetStub = sandbox.stub().returns(undefined); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, + } as any; + + const nestedChildrenAddStub = sandbox.stub(); + const nestedChildrenGetStub = sandbox.stub().returns(undefined); + const mockNestedNode: TestItem = { + id: 'nested-id', + canResolveChildren: true, + tags: [], + children: { add: nestedChildrenAddStub, get: nestedChildrenGetStub }, + } as any; + + const mockNestedTestItem: TestItem = { + id: 'nested-test-id', + tags: [], + } as any; + + createTestItemStub.onFirstCall().returns(mockNestedNode); + createTestItemStub.onSecondCall().returns(mockNestedTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should create nested node - uses child.id_ for non-leaf nodes + assert.ok(createTestItemStub.calledWith('nested-id', 'NestedFolder', sinon.match.any)); + assert.ok(rootChildrenAddStub.calledWith(mockNestedNode)); + assert.strictEqual(mockNestedNode.canResolveChildren, true); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + + // Should create nested test item - uses child.id_ for test items too + assert.ok(createTestItemStub.calledWith('nested-test-id', 'nested_test', sinon.match.any)); + assert.ok(nestedChildrenAddStub.calledWith(mockNestedTestItem)); + }); + + test('should reuse existing nodes when they already exist', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── ExistingFolder (folder, already exists) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/existing', + name: 'ExistingFolder', + type_: 'folder', + id_: 'existing-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const existingChildrenAddStub = sandbox.stub(); + const existingChildrenGetStub = sandbox.stub().returns(undefined); + const existingNode: TestItem = { + id: 'existing-id', + children: { add: existingChildrenAddStub, get: existingChildrenGetStub }, + } as any; + const rootChildrenGetStub = sandbox.stub().withArgs('existing-id').returns(existingNode); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + } as any; + + // Mock existing node in testController.items + itemsGetStub.withArgs('/test/path/existing').returns(existingNode); + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should not create a new node, should reuse existing one + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + // Should not create a new node for the existing folder + assert.ok(createTestItemStub.neverCalledWith('existing-id', 'ExistingFolder', sinon.match.any)); + assert.ok(existingChildrenAddStub.calledWith(mockTestItem)); + // Should not add existing node to root children again + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should respect cancellation token and stop processing', () => { + // Arrange + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test1', + type_: 'test', + id_: 'test1-id', + lineno: 10, + runID: 'run-id-1', + }; + + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test2', + type_: 'test', + id_: 'test2-id', + lineno: 20, + runID: 'run-id-2', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Set cancellation token to be cancelled + const cancelledToken = { + isCancellationRequested: true, + onCancellationRequested: sandbox.stub(), + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelledToken); + + // Assert - no test items should be created when cancelled + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + assert.strictEqual(resultResolver.runIdToTestItem.size, 0); + }); + + test('should handle empty children array gracefully', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert - should complete without errors + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should add correct tags to all created items', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const mockRootItem: TestItem = { + id: 'root-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, + } as any; + + const mockNestedNode: TestItem = { + id: 'nested-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + canResolveChildren: false, + } as any; + + createTestItemStub.onCall(0).returns(mockRootItem); + createTestItemStub.onCall(1).returns(mockNestedNode); + createTestItemStub.onCall(2).returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert - All items should have RunTestTag and DebugTestTag + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + test('should handle a test node with no lineno property', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── test_without_lineno (test, no lineno) + const testItem = { + path: '/test/path/test.py', + name: 'test_without_lineno', + type_: 'test', + id_: 'test-no-lineno-id', + runID: 'run-id-no-lineno', + } as DiscoveredTestItem; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-no-lineno-id', + label: 'test_without_lineno', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-no-lineno-id', 'test_without_lineno', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + // range is undefined since lineno is not provided + assert.strictEqual(mockTestItem.range, undefined); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should handle a node with multiple children', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // ├── test_one (test) + // └── test_two (test) + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test_one', + type_: 'test', + id_: 'test-one-id', + lineno: 3, + runID: 'run-id-one', + }; + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test_two', + type_: 'test', + id_: 'test-two-id', + lineno: 7, + runID: 'run-id-two', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem1: TestItem = { + id: 'test-one-id', + label: 'test_one', + uri: Uri.file('/test/path/test1.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(2, 0), new Position(3, 0)), + } as any; + const mockTestItem2: TestItem = { + id: 'test-two-id', + label: 'test_two', + uri: Uri.file('/test/path/test2.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(6, 0), new Position(7, 0)), + } as any; + + createTestItemStub.onFirstCall().returns(mockTestItem1); + createTestItemStub.onSecondCall().returns(mockTestItem2); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledWith('test-one-id', 'test_one', sinon.match.any)); + assert.ok(createTestItemStub.calledWith('test-two-id', 'test_two', sinon.match.any)); + // two test items called with mockRootItem's method childrenAddStub + assert.strictEqual(childrenAddStub.callCount, 2); + assert.deepStrictEqual(mockTestItem1.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem2.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem1.range, new Range(new Position(2, 0), new Position(3, 0))); + assert.deepStrictEqual(mockTestItem2.range, new Range(new Position(6, 0), new Position(7, 0))); + }); +}); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts new file mode 100644 index 000000000000..6d2895ca2979 --- /dev/null +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -0,0 +1,521 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; + +import { TestController, TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; +import { IConfigurationService } from '../../../client/common/types'; +import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 +import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; +import * as Telemetry from '../../../client/telemetry'; +import { EventName } from '../../../client/telemetry/constants'; +import { ITestResultResolver } from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; + +suite('Workspace test adapter', () => { + suite('Test discovery', () => { + let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; + + let discoverTestsStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + + let telemetryEvent: { eventName: EventName; properties: Record<string, unknown> }[] = []; + let execFactory: typemoq.IMock<IPythonExecutionFactory>; + + // Stubbed test controller (see comment around L.40) + let testController: TestController; + let log: string[] = []; + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['--foo'] }, + }), + } as unknown) as IConfigurationService; + + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + } as unknown) as ITestResultResolver; + + // const vsIdToRunIdGetStub = sinon.stub(stubResultResolver.vsIdToRunId, 'get'); + // const expectedRunId = 'expectedRunId'; + // vsIdToRunIdGetStub.withArgs(sinon.match.any).returns(expectedRunId); + + // For some reason the 'tests' namespace in vscode returns undefined. + // While I figure out how to expose to the tests, they will run + // against a stub test controller and stub test items. + const testItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + createTestItem: () => { + log.push('createTestItem'); + return testItem; + }, + dispose: () => { + // empty + }, + } as unknown) as TestController; + + // testController = tests.createTestController('mock-python-tests', 'Mock Python Tests'); + + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | Record<string, number> | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record<string, unknown>, + }); + }; + + discoverTestsStub = sinon.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); + sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + }); + + teardown(() => { + telemetryEvent = []; + log = []; + testController.dispose(); + sinon.restore(); + }); + + test('If discovery failed correctly create error node', async () => { + discoverTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const uriFoo = Uri.parse('foo'); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + uriFoo, + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + execFactory = typemoq.Mock.ofType<IPythonExecutionFactory>(); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, uriFoo, sinon.match.any, testProvider); + }); + + test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { + discoverTestsStub.resolves(); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledOnce(discoverTestsStub); + }); + + test('If discovery is already running, do not call discoveryAdapter.discoverTests again', async () => { + discoverTestsStub.callsFake( + async () => + new Promise<void>((resolve) => { + setTimeout(() => { + // Simulate time taken by discovery. + resolve(); + }, 2000); + }), + ); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + // Try running discovery twice + const one = workspaceTestAdapter.discoverTests(testController, execFactory.object); + const two = workspaceTestAdapter.discoverTests(testController, execFactory.object); + + Promise.all([one, two]); + + sinon.assert.calledOnce(discoverTestsStub); + }); + + test('If discovery succeeds, send a telemetry event with the "failed" key set to false', async () => { + discoverTestsStub.resolves({ status: 'success' }); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); + assert.strictEqual(telemetryEvent.length, 2); + + const lastEvent = telemetryEvent[1]; + assert.strictEqual(lastEvent.properties.failed, false); + }); + + test('If discovery failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { + discoverTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); + assert.strictEqual(telemetryEvent.length, 2); + + const lastEvent = telemetryEvent[1]; + assert.ok(lastEvent.properties.failed); + }); + }); + suite('Test execution workspace test adapter', () => { + let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; + let executionTestsStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let runInstance: typemoq.IMock<TestRun>; + let testControllerMock: typemoq.IMock<TestController>; + let telemetryEvent: { eventName: EventName; properties: Record<string, unknown> }[] = []; + let resultResolver: ResultResolver.PythonResultResolver; + let execFactory: typemoq.IMock<IPythonExecutionFactory>; + + // Stubbed test controller (see comment around L.40) + let testController: TestController; + let log: string[] = []; + + const sandbox = sinon.createSandbox(); + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['--foo'] }, + }), + } as unknown) as IConfigurationService; + + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + vsIdToRunId: { + get: sinon.stub().returns('expectedRunId'), + }, + } as unknown) as ITestResultResolver; + const testItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + createTestItem: () => { + log.push('createTestItem'); + return testItem; + }, + dispose: () => { + // empty + }, + } as unknown) as TestController; + + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | Record<string, number> | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record<string, unknown>, + }); + }; + + executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); + sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + execFactory = typemoq.Mock.ofType<IPythonExecutionFactory>(); + runInstance = typemoq.Mock.ofType<TestRun>(); + + const testProvider = 'pytest'; + const workspaceUri = Uri.file('foo'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + }); + + teardown(() => { + telemetryEvent = []; + log = []; + testController.dispose(); + sandbox.restore(); + }); + test('When executing tests, the right tests should be sent to be executed', async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + resultResolver, + ); + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => + // Custom implementation logic here based on the provided testNode and collection + + // Example implementation: returning a predefined array of TestItem objects + [testNode], + ); + + const mockTestItem1 = createMockTestItem('mockTestItem1'); + const mockTestItem2 = createMockTestItem('mockTestItem2'); + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + // Add as many mock TestItems as needed + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testControllerMock = typemoq.Mock.ofType<TestController>(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + await workspaceTestAdapter.executeTests( + testController, + runInstance.object, + [mockTestItem1, mockTestItem2], + execFactory.object, + ); + + runInstance.verify((r) => r.started(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test("When executing tests, the workspace test adapter should call the test execute adapter's executionTest method", async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution is already running, do not call executionAdapter.runTests again', async () => { + executionTestsStub.callsFake( + async () => + new Promise<void>((resolve) => { + setTimeout(() => { + // Simulate time taken by discovery. + resolve(); + }, 2000); + }), + ); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + // Try running discovery twice + const one = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + const two = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + Promise.all([one, two]); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution failed correctly create error node', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); + }); + + test('If execution failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_RUN_ALL_FAILED); + assert.strictEqual(telemetryEvent.length, 1); + }); + }); +}); + +function createMockTestItem(id: string): TestItem { + const range = typemoq.Mock.ofType<Range>(); + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/unittest/unittest.argsService.unit.test.ts b/src/test/testing/unittest/unittest.argsService.unit.test.ts deleted file mode 100644 index 8c8ad61ae446..000000000000 --- a/src/test/testing/unittest/unittest.argsService.unit.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index 7904441997d1..000000000000 --- a/src/test/testing/unittest/unittest.diagnosticService.unit.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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 deleted file mode 100644 index 3f5e93772e91..000000000000 --- a/src/test/testing/unittest/unittest.discovery.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -// 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.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 deleted file mode 100644 index 1d03baada13e..000000000000 --- a/src/test/testing/unittest/unittest.discovery.unit.test.ts +++ /dev/null @@ -1,625 +0,0 @@ -// 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 deleted file mode 100644 index e3585527b04c..000000000000 --- a/src/test/testing/unittest/unittest.run.test.ts +++ /dev/null @@ -1,519 +0,0 @@ -// 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.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 deleted file mode 100644 index 858cefbc0a91..000000000000 --- a/src/test/testing/unittest/unittest.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -'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 deleted file mode 100644 index 58d771ca9c60..000000000000 --- a/src/test/testing/unittest/unittest.unit.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -// 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/testing/utils.unit.test.ts b/src/test/testing/utils.unit.test.ts new file mode 100644 index 000000000000..8efa0cee0e65 --- /dev/null +++ b/src/test/testing/utils.unit.test.ts @@ -0,0 +1,51 @@ +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as utils from '../../client/testing/utils'; +import sinon from 'sinon'; +use(chaiAsPromised.default); + +function test_idToModuleClassMethod() { + try { + expect(utils.idToModuleClassMethod('foo')).to.equal('foo'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClass')).to.equal('c.MyClass'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClassmy_method')).to.equal('c.MyClass.my_method'); + expect(utils.idToModuleClassMethod('\\MyClass')).to.be.undefined; + console.log('test_idToModuleClassMethod passed'); + } catch (e) { + console.error('test_idToModuleClassMethod failed:', e); + } +} + +async function test_writeTestIdToClipboard() { + let clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves(); + const { writeTestIdToClipboard } = utils; + try { + // unittest id + const testItem = { id: 'a/b/c.pyMyClass\\my_method' }; + await writeTestIdToClipboard(testItem as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'c.MyClass.my_method'); + clipboardStub.resetHistory(); + + // pytest id + const testItem2 = { id: 'tests/test_foo.py::TestClass::test_method' }; + await writeTestIdToClipboard(testItem2 as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'tests/test_foo.py::TestClass::test_method'); + clipboardStub.resetHistory(); + + // undefined + await writeTestIdToClipboard(undefined as any); + sinon.assert.notCalled(clipboardStub); + + console.log('test_writeTestIdToClipboard passed'); + } catch (e) { + console.error('test_writeTestIdToClipboard failed:', e); + } finally { + sinon.restore(); + } +} + +// Run tests +(async () => { + test_idToModuleClassMethod(); + await test_writeTestIdToClipboard(); +})(); diff --git a/src/test/textUtils.ts b/src/test/textUtils.ts index 36c877887130..85308213fd56 100644 --- a/src/test/textUtils.ts +++ b/src/test/textUtils.ts @@ -22,6 +22,6 @@ export function compareFiles(expectedContent: string, actualContent: string) { actualLines.length, expectedLines.length > actualLines.length ? 'Actual contains more lines than expected' - : 'Expected contains more lines than the actual' + : 'Expected contains more lines than the actual', ).to.be.equal(expectedLines.length); } diff --git a/src/test/unittests.ts b/src/test/unittests.ts index 066d4b628012..dc4e79cbbff3 100644 --- a/src/test/unittests.ts +++ b/src/test/unittests.ts @@ -10,12 +10,10 @@ 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'); } @@ -24,15 +22,14 @@ 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) => { @@ -47,14 +44,14 @@ const Module = require('module'); 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 @@ -65,21 +62,9 @@ const Module = require('module'); 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 index f92ffbfd91c6..13f46bd38f82 100644 --- a/src/test/utils/fs.ts +++ b/src/test/utils/fs.ts @@ -3,13 +3,15 @@ 'use strict'; +import * as fsapi from '../../client/common/platform/fs-paths'; +import * as path from 'path'; import * as tmp from 'tmp'; +import { parseTree } from '../../client/common/utils/text'; export function createTemporaryFile( extension: string, - temporaryDirectory?: string + temporaryDirectory?: string, ): Promise<{ filePath: string; cleanupCallback: Function }> { - // tslint:disable-next-line:no-any const options: any = { postfix: extension }; if (temporaryDirectory) { options.dir = temporaryDirectory; @@ -24,3 +26,298 @@ export function createTemporaryFile( }); }); } + +// Something to consider: we should combine with `createDeclaratively` +// (in src/test/testing/results.ts). + +type FileKind = 'dir' | 'file' | 'exe' | 'symlink'; + +/** + * Extract the name and kind for the given entry from a text FS tree. + * + * As with `parseFSTree()`, the expected path separator is forward slash + * (`/`) regardless of the OS. This allows for consistent usage. + * + * If an entry has a trailing slash then it is a directory. Otherwise + * it is a file. Angle brackets(`<>`) around an entry indicate it is + * an executable file. (Directories cannot be marked as executable.) + * + * Only directory entries can have slashes, both at the end and anywhere + * else. However, only root entries (`opts.topLevel === true`) can have + * a leading slash. + * + * @returns - the entry's name (without markers) and kind + * + * Examples (valid): + * + * `/x/a_root/` `['/x/a_root', 'dir']` # if "topLevel" + * `./x/y/z/a_root/` `['./x/y/z/a_root', 'dir']` # if "topLevel" + * `some_dir/` `['some_dir`, 'dir']` + * `spam` `['spam', 'file']` + * `x/y/z/spam` `['x/y/z/spam', 'file']` + * `<spam>` `['spam', 'exe']` + * `<x/y/z/spam>` `['x/y/z/spam', 'exe']` + * `<spam.exe> `['spam.exe', 'exe']` + * + * Examples (valid but unlikely usage): + * + * `x/y/z/some_dir/` `['x/y/z/some_dir', 'dir']` # inline parents + * + * Examples (invalid): + * + * `/x/y/z/a_root/` # if not "topLevel" + * `./x/a_root/` ` # if not "topLevel" + * `../a_root/` # moving above CWD + * `x/y/../z/` # unnormalized + * `x/y/./z/` # unnormalized + * `<some_dir/>` # directories cannot be marked as executable + * `<some_dir>/` # directories cannot be marked as executable + * `<spam` # missing closing bracket + * `spam>` # missing opening bracket + */ +function parseFSEntry( + entry: string, + opts: { + topLevel?: boolean; + allowInlineParents?: boolean; + } = {}, +): [string | [string, string], FileKind] { + let text = entry; + let symlinkTarget = ''; + if (text.startsWith('|')) { + text = text.slice(1); + } else { + // Deal with executables. + if (text.match(/^<[^/<>]+>$/)) { + const name = text.slice(1, -1); + return [name, 'exe']; + } + // Deal with symlinks. + const parts = text.split(' -> ', 2); + if (parts.length == 2) { + [text, symlinkTarget] = parts; + if (text.endsWith('/')) { + throw Error(`bad symlink "${entry}"`); + } + if (symlinkTarget.includes('<') || symlinkTarget.includes('>')) { + throw Error(`bad entry "${entry}"`); + } + } + // It must be a regular file or directory. + if (text.includes('<') || text.includes('>')) { + throw Error(`bad entry "${entry}"`); + } + } + + // Make sure the entry is normalized. + const candidate = text.startsWith('./') ? text.slice(1) : text; + if (path.posix.normalize(candidate) !== candidate || text.startsWith('../')) { + throw Error(`expected normalized path, got "${entry}"`); + } + + // Handle "top-level" entries. + if (opts.topLevel) { + if (!text.endsWith('/')) { + throw Error(`expected directory at top level, got "${entry}"`); + } + if (!text.startsWith('/') && !text.startsWith('./')) { + throw Error(`expected prefix for top level, got "${entry}"`); + } + return [text, 'dir']; + } + + // Handle other entries. + let relname: string; + let reltext: string | [string, string]; + let kind: FileKind; + if (text.endsWith('/')) { + kind = 'dir'; + relname = text.slice(0, -1); + reltext = text; + } else if (symlinkTarget !== '') { + kind = 'symlink'; + relname = text; + reltext = [relname, symlinkTarget]; + } else { + kind = 'file'; + relname = text; + reltext = text; + } + if (relname.includes('/') && !opts.allowInlineParents) { + throw Error(`did not expect inline parents, got "${entry}"`); + } + if (relname.startsWith('/') || relname.startsWith('./')) { + throw Error(`expected relative path, got "${entry}"`); + } + return [reltext, kind]; +} + +/** + * Extract the directory tree represented by the given text.' + * + * "/" is the expected path separator, regardless of current OS. + * Directories always end with "/". Executables are surrounded + * by angle brackets "<>". See `parseFSEntry()` for more info. + * + * @returns - the flat list of (filename, parentdir, kind) for each + * node in the tree + * + * Example: + * + * parseFSTree(` + * ./x/y/z/root1/ + * dir1/ + * file1 + * subdir1_1/ + * # empty + * subdir1_2/ + * file2 + * <file3> + * <file4> + * file5 + * dir2/ + * file6 + * <file7> + * ./x/y/z/root2/ + * dir3/ + * subdir3_1/ + * file8 + * ./a/b/root3/ + * <file9> + * `.trim()) + * + * would produce the following: + * + * [ + * ['CWD/x/y/z/root1', '', 'dir'], + * ['CWD/x/y/z/root1/dir1', 'CWD/x/y/z/root1', 'dir'], + * ['CWD/x/y/z/root1/dir1/file1', 'CWD/x/y/z/root1/dir1', 'file'], + * ['CWD/x/y/z/root1/dir1/subdir1_1', 'CWD/x/y/z/root1/dir1', 'dir'], + * ['CWD/x/y/z/root1/dir1/subdir1_2', 'CWD/x/y/z/root1/dir1', 'dir'], + * ['CWD/x/y/z/root1/dir1/subdir1_2/file2', 'CWD/x/y/z/root1/dir1/subdir1_2', 'file'], + * ['CWD/x/y/z/root1/dir1/subdir1_2/file3', 'CWD/x/y/z/root1/dir1/subdir1_2', 'exe'], + * ['CWD/x/y/z/root1/dir1/file4', 'CWD/x/y/z/root1/dir1', 'exe'], + * ['CWD/x/y/z/root1/dir1/file5', 'CWD/x/y/z/root1/dir1', 'file'], + * ['CWD/x/y/z/root1/dir2', 'CWD/x/y/z/root1', 'dir'], + * ['CWD/x/y/z/root1/dir2/file6', 'CWD/x/y/z/root1/dir2', 'file'], + * ['CWD/x/y/z/root1/dir2/file7', 'CWD/x/y/z/root1/dir2', 'exe'], + * + * ['CWD/x/y/z/root2', '', 'dir'], + * ['CWD/x/y/z/root2/dir3', 'CWD/x/y/z/root2', 'dir'], + * ['CWD/x/y/z/root2/dir3/subdir3_1', 'CWD/x/y/z/root2/dir3', 'dir'], + * ['CWD/x/y/z/root2/dir3/subdir3_1/file8', 'CWD/x/y/z/root2/dir3/subdir3_1', 'file'], + * + * ['CWD/a/b/root3', '', 'dir'], + * ['CWD/a/b/root3/file9', 'CWD/a/b/root3', 'exe'], + * ] + */ +export function parseFSTree( + text: string, + // Use process.cwd() by default. + cwd?: string, +): [string | [string, string], string, FileKind][] { + const curDir = cwd ?? process.cwd(); + const parsed: [string | [string, string], string, FileKind][] = []; + + const entries = parseTree(text); + entries.forEach((data) => { + const [entry, parentIndex] = data; + const opts = { + topLevel: parentIndex === -1, + allowInlineParents: false, + }; + const [relname, kind] = parseFSEntry(entry, opts); + let fullname: string | [string, string]; + let parentFilename: string; + if (parentIndex === -1) { + parentFilename = ''; + fullname = path.resolve(curDir, relname as string); + } else { + if (typeof parsed[parentIndex][0] !== 'string') { + throw Error(`parent can't be a symlink, got ${parsed[parentIndex]} (for ${kind} ${relname})`); + } + parentFilename = parsed[parentIndex][0] as string; + if (kind === 'symlink') { + let [target, symlink] = relname as [string, string]; + target = path.join(parentFilename, target); + symlink = path.join(parentFilename, symlink); + fullname = [target, symlink]; + } else { + fullname = path.join(parentFilename, relname as string); + } + } + parsed.push([fullname, parentFilename, kind]); + }); + + return parsed; +} + +/** + * Mirror the directory tree (represented by the given text) on disk. + * + * See `parseFSTree()` for the "spec" format. + */ +export async function ensureFSTree( + spec: string, + // Use process.cwd() by default. + cwd?: string, +): Promise<string[]> { + const roots: string[] = []; + const promises = parseFSTree(spec, cwd) + // Now ensure each entry exists. + .map(async (data) => { + const [filename, parentFilename, kind] = data; + + try { + if (kind === 'dir') { + await fsapi.ensureDir(filename as string); + } else if (kind === 'exe') { + await ensureExecutable(filename as string); + } else if (kind === 'file') { + // "touch" the file. + await fsapi.ensureFile(filename as string); + } else if (kind === 'symlink') { + const [symlink, target] = filename as [string, string]; + await ensureSymlink(target, symlink); + } else { + throw Error(`unsupported file kind ${kind}`); + } + } catch (err) { + console.log('FAILED:', err); + throw err; + } + + if (parentFilename === '') { + roots.push(filename as string); + } + }); + await Promise.all(promises); + return roots; +} + +async function ensureExecutable(filename: string): Promise<void> { + // "touch" the file. + await fsapi.ensureFile(filename as string); + await fsapi.chmod(filename as string, 0o755); +} + +async function ensureSymlink(target: string, filename: string): Promise<void> { + try { + await fsapi.ensureSymlink(target, filename); + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code === 'ENOENT') { + // The target doesn't exist. Make the symlink anyway. + try { + await fsapi.symlink(target, filename); + } catch (err) { + const symlinkError = err as NodeJS.ErrnoException; + if (symlinkError.code !== 'EEXIST') { + throw err; // re-throw + } + } + } else { + throw err; // re-throw + } + } +} diff --git a/src/test/utils/interpreters.ts b/src/test/utils/interpreters.ts index ec277a08d589..ece3b7731c5c 100644 --- a/src/test/utils/interpreters.ts +++ b/src/test/utils/interpreters.ts @@ -4,17 +4,13 @@ 'use strict'; import { Architecture } from '../../client/common/utils/platform'; -import { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info'; +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<PythonInterpreter>} [info] - * @returns {PythonInterpreter} */ -export function createPythonInterpreter(info?: Partial<PythonInterpreter>): PythonInterpreter { +export function createPythonInterpreter(info?: Partial<PythonEnvironment>): PythonEnvironment { const rnd = new Date().getTime().toString(); return { displayName: `Something${rnd}`, @@ -22,7 +18,7 @@ export function createPythonInterpreter(info?: Partial<PythonInterpreter>): Pyth path: `somePath${rnd}`, sysPrefix: `someSysPrefix${rnd}`, sysVersion: `1.1.1`, - type: InterpreterType.Unknown, - ...(info || {}) + envType: EnvironmentType.Unknown, + ...(info || {}), }; } diff --git a/src/test/utils/vscode.ts b/src/test/utils/vscode.ts new file mode 100644 index 000000000000..4364c507c36f --- /dev/null +++ b/src/test/utils/vscode.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import * as fs from '../../client/common/platform/fs-paths'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; + +const insidersVersion = /^\^(\d+\.\d+\.\d+)-(insider|\d{8})$/; + +export function getChannel(): string { + if (process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL) { + return process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL; + } + const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); + if (fs.pathExistsSync(packageJsonPath)) { + const packageJson = fs.readJSONSync(packageJsonPath); + const engineVersion = packageJson.engines.vscode; + if (insidersVersion.test(engineVersion)) { + // Can't pass in the version number for an insiders build; + // https://github.com/microsoft/vscode-test/issues/176 + return 'insiders'; + } + return engineVersion.replace('^', ''); + } + return 'stable'; +} diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 2ee192f112e1..b7ea2bc549a0 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -3,23 +3,22 @@ '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'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { TestItem } from 'vscode'; const Module = require('module'); type VSCode = typeof vscode; const mockedVSCode: Partial<VSCode> = {}; -export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock<VSCode[P]> } = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: 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; + const mockedObj = mock<VSCode[K]>(); + (mockedVSCode as any)[name] = instance(mockedObj); mockedVSCodeNamespaces[name] = mockedObj as any; } @@ -37,22 +36,33 @@ export function initialize() { generateMock('window'); generateMock('commands'); generateMock('languages'); + generateMock('extensions'); generateMock('env'); generateMock('debug'); generateMock('scm'); - generateNotebookMocks(); + generateMock('notebooks'); // 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(mockedVSCodeNamespaces.env!.clipboard).thenReturn(clipboard); + when(mockedVSCodeNamespaces.env!.appName).thenReturn('Insider'); + + // This API is used in src/client/telemetry/telemetry.ts + const extension = mock<vscode.Extension<any>>(); + const packageJson = mock<any>(); + const contributes = mock<any>(); + when(extension.packageJSON).thenReturn(instance(packageJson)); + when(packageJson.contributes).thenReturn(instance(contributes)); + when(contributes.debuggers).thenReturn([{ aiKey: '' }]); + when(mockedVSCodeNamespaces.extensions!.getExtension(anything())).thenReturn(instance(extension)); + when(mockedVSCodeNamespaces.extensions!.all).thenReturn([]); // 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') { + if (request === '@vscode/extension-telemetry') { return { default: vscMockTelemetryReporter as any }; } // less files need to be in import statements to be converted to css @@ -64,23 +74,32 @@ export function initialize() { }; } -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.ThemeIcon = vscodeMocks.ThemeIcon; +mockedVSCode.l10n = vscodeMocks.l10n; +mockedVSCode.ThemeColor = vscodeMocks.ThemeColor; +mockedVSCode.MarkdownString = vscodeMocks.MarkdownString; +mockedVSCode.Hover = vscodeMocks.Hover; +mockedVSCode.Disposable = vscodeMocks.Disposable as any; +mockedVSCode.ExtensionKind = vscodeMocks.ExtensionKind; +mockedVSCode.CodeAction = vscodeMocks.CodeAction; +mockedVSCode.TestMessage = vscodeMocks.TestMessage; +mockedVSCode.Location = vscodeMocks.Location; +mockedVSCode.EventEmitter = vscodeMocks.EventEmitter; +mockedVSCode.CancellationTokenSource = vscodeMocks.CancellationTokenSource; +mockedVSCode.CompletionItemKind = vscodeMocks.CompletionItemKind; +mockedVSCode.SymbolKind = vscodeMocks.SymbolKind; +mockedVSCode.IndentAction = vscodeMocks.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.CallHierarchyItem = vscodeMocks.vscMockExtHostedTypes.CallHierarchyItem; mockedVSCode.CompletionItem = vscodeMocks.vscMockExtHostedTypes.CompletionItem; mockedVSCode.CompletionItemKind = vscodeMocks.vscMockExtHostedTypes.CompletionItemKind; mockedVSCode.CodeLens = vscodeMocks.vscMockExtHostedTypes.CodeLens; +mockedVSCode.Diagnostic = vscodeMocks.vscMockExtHostedTypes.Diagnostic; mockedVSCode.DiagnosticSeverity = vscodeMocks.vscMockExtHostedTypes.DiagnosticSeverity; mockedVSCode.SnippetString = vscodeMocks.vscMockExtHostedTypes.SnippetString; mockedVSCode.ConfigurationTarget = vscodeMocks.vscMockExtHostedTypes.ConfigurationTarget; @@ -95,30 +114,64 @@ 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 as any).CodeActionKind = vscodeMocks.CodeActionKind; +mockedVSCode.CompletionItemKind = vscodeMocks.CompletionItemKind; +mockedVSCode.CompletionTriggerKind = vscodeMocks.CompletionTriggerKind; +mockedVSCode.DebugAdapterExecutable = vscodeMocks.DebugAdapterExecutable; +mockedVSCode.DebugAdapterServer = vscodeMocks.DebugAdapterServer; mockedVSCode.QuickInputButtons = vscodeMocks.vscMockExtHostedTypes.QuickInputButtons; -mockedVSCode.FileType = vscodeMocks.vscMock.FileType; +mockedVSCode.FileType = vscodeMocks.FileType; +mockedVSCode.UIKind = vscodeMocks.UIKind; mockedVSCode.FileSystemError = vscodeMocks.vscMockExtHostedTypes.FileSystemError; -(mockedVSCode as any).CellKind = vscodeMocks.vscMockExtHostedTypes.CellKind; +mockedVSCode.LanguageStatusSeverity = vscodeMocks.LanguageStatusSeverity; +mockedVSCode.QuickPickItemKind = vscodeMocks.QuickPickItemKind; +mockedVSCode.InlayHint = vscodeMocks.InlayHint; +mockedVSCode.LogLevel = vscodeMocks.LogLevel; +(mockedVSCode as any).NotebookCellKind = vscodeMocks.vscMockExtHostedTypes.NotebookCellKind; (mockedVSCode as any).CellOutputKind = vscodeMocks.vscMockExtHostedTypes.CellOutputKind; (mockedVSCode as any).NotebookCellRunState = vscodeMocks.vscMockExtHostedTypes.NotebookCellRunState; +(mockedVSCode as any).TypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.TypeHierarchyItem; +(mockedVSCode as any).ProtocolTypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.ProtocolTypeHierarchyItem; +(mockedVSCode as any).CancellationError = vscodeMocks.vscMockExtHostedTypes.CancellationError; +(mockedVSCode as any).LSPCancellationError = vscodeMocks.vscMockExtHostedTypes.LSPCancellationError; +mockedVSCode.TestRunProfileKind = vscodeMocks.TestRunProfileKind; +(mockedVSCode as any).TestCoverageCount = class TestCoverageCount { + constructor(public covered: number, public total: number) {} +}; +(mockedVSCode as any).FileCoverage = class FileCoverage { + constructor( + public uri: any, + public statementCoverage: any, + public branchCoverage?: any, + public declarationCoverage?: any, + ) {} +}; +(mockedVSCode as any).StatementCoverage = class StatementCoverage { + constructor(public executed: number | boolean, public location: any, public branches?: any) {} +}; -// 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; +// Mock TestController for vscode.tests namespace +function createMockTestController(): vscode.TestController { + const disposable = { dispose: () => undefined }; + return ({ + items: { + forEach: () => undefined, + get: () => undefined, + add: () => undefined, + replace: () => undefined, + delete: () => undefined, + size: 0, + [Symbol.iterator]: function* () {}, + }, + createRunProfile: () => disposable, + createTestItem: () => ({} as TestItem), + dispose: () => undefined, + resolveHandler: undefined, + refreshHandler: undefined, + } as unknown) as vscode.TestController; } + +// Add tests namespace with createTestController +(mockedVSCode as any).tests = { + createTestController: (_id: string, _label: string) => createMockTestController(), +}; diff --git a/src/test/workspaceSymbols/common.ts b/src/test/workspaceSymbols/common.ts deleted file mode 100644 index b9635d0d139d..000000000000 --- a/src/test/workspaceSymbols/common.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 6bfa1d77301a..000000000000 --- a/src/test/workspaceSymbols/generator.unit.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// 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 deleted file mode 100644 index 35b8f101d2d7..000000000000 --- a/src/test/workspaceSymbols/main.unit.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -// 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 deleted file mode 100644 index 3cd753443a5f..000000000000 --- a/src/test/workspaceSymbols/provider.unit.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// 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 deleted file mode 100644 index 4739b4629cfb..000000000000 --- a/src/testMultiRootWkspc/disableLinters/.vscode/tags +++ /dev/null @@ -1,19 +0,0 @@ -!_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/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 9d64c3f8b53b..51d218783041 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -37,12 +37,6 @@ "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 index 2c4fa010619f..0967ef424bce 100644 --- a/src/testMultiRootWkspc/parent/child/.vscode/settings.json +++ b/src/testMultiRootWkspc/parent/child/.vscode/settings.json @@ -1,3 +1 @@ -{ - "python.workspaceSymbols.enabled": false -} +{} diff --git a/src/testMultiRootWkspc/parent/child/.vscode/tags b/src/testMultiRootWkspc/parent/child/.vscode/tags deleted file mode 100644 index e6791c755b0f..000000000000 --- a/src/testMultiRootWkspc/parent/child/.vscode/tags +++ /dev/null @@ -1,24 +0,0 @@ -!_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/smokeTests/create_delete_file.py b/src/testMultiRootWkspc/smokeTests/create_delete_file.py new file mode 100644 index 000000000000..399bc4863c15 --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/create_delete_file.py @@ -0,0 +1,5 @@ +with open('smart_send_smoke.txt', 'w') as f: + f.write('This is for smart send smoke test') +import os + +os.remove('smart_send_smoke.txt') diff --git a/src/testMultiRootWkspc/smokeTests/definitions.ipynb b/src/testMultiRootWkspc/smokeTests/definitions.ipynb new file mode 100644 index 000000000000..ee5427bbff0f --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/definitions.ipynb @@ -0,0 +1,88 @@ +{ + "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, + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from contextlib import contextmanager" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def my_decorator(fn):\n", + " \"\"\"\n", + " This is my decorator.\n", + " \"\"\"\n", + " def wrapper(*args, **kwargs):\n", + " \"\"\"\n", + " This is the wrapper.\n", + " \"\"\"\n", + " return 42\n", + " return wrapper\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@my_decorator\n", + "def thing(arg):\n", + " \"\"\"\n", + " Thing which is decorated.\n", + " \"\"\"\n", + " pass\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@contextmanager\n", + "def my_context_manager():\n", + " \"\"\"\n", + " This is my context manager.\n", + " \"\"\"\n", + " print(\"before\")\n", + " yield\n", + " print(\"after\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with my_context_manager():\n", + " thing(19)" + ] + } + ] +} \ No newline at end of file diff --git a/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/config.py b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/config.py new file mode 100644 index 000000000000..dee2d1ae9a6b --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/config.py @@ -0,0 +1,114 @@ +# The default ``config.py`` +# flake8: noqa + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git', '.tox'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + # prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + # prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + # prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + # If `True`, rope will insert new module imports as + # `from <package> import <module>` by default. + prefs['prefer_module_from_imports'] = False + + # If `True`, rope will transform a comma list of imports into + # multiple separate import statements when organizing + # imports. + prefs['split_imports'] = False + + # If `True`, rope will remove all top-level import statements and + # reinsert them at the top of the module when making changes. + prefs['pull_imports_to_top'] = True + + # If `True`, rope will sort imports alphabetically by module name instead + # of alphabetically by import statement, with from imports after normal + # imports. + prefs['sort_imports_alphabetically'] = False + + # Location of implementation of + # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general + # case, you don't have to change this value, unless you're an rope expert. + # Change this value to inject you own implementations of interfaces + # listed in module rope.base.oi.type_hinting.providers.interfaces + # For example, you can add you own providers for Django Models, or disable + # the search type-hinting in a class hierarchy, etc. + prefs['type_hinting_factory'] = ( + 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/objectdb b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/objectdb new file mode 100644 index 000000000000..0a47446c0ad2 Binary files /dev/null and b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/objectdb differ diff --git a/src/testMultiRootWkspc/workspace1/.vscode/tags b/src/testMultiRootWkspc/workspace1/.vscode/tags deleted file mode 100644 index 4739b4629cfb..000000000000 --- a/src/testMultiRootWkspc/workspace1/.vscode/tags +++ /dev/null @@ -1,19 +0,0 @@ -!_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 index 439f899e9e22..6aceaad5e020 100644 --- a/src/testMultiRootWkspc/workspace1/file.py +++ b/src/testMultiRootWkspc/workspace1/file.py @@ -10,78 +10,78 @@ def __init__(self): def meth1(self, arg): """this issues a message""" - print self + print(self) def meth2(self, arg): """and this one not""" # pylint: disable=unused-argument - print self\ - + "foo" + print(self\ + + "foo") def meth3(self): """test one line disabling""" # no error - print self.bla # pylint: disable=no-member + print(self.bla) # pylint: disable=no-member # error - print self.blop + print(self.blop) def meth4(self): """test re-enabling""" # pylint: disable=no-member # no error - print self.bla - print self.blop + print(self.bla) + print(self.blop) # pylint: enable=no-member # error - print self.blip + print(self.blip) def meth5(self): """test IF sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print(self.bla) if self.blop: # pylint: enable=no-member # error - print self.blip + print(self.blip) else: # no error - print self.blip + print(self.blip) # no error - print self.blip + print(self.blip) def meth6(self): """test TRY/EXCEPT sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print(self.bla) try: # pylint: enable=no-member # error - print self.blip + print(self.blip) except UndefinedName: # pylint: disable=undefined-variable # no error - print self.blip + print(self.blip) # no error - print self.blip + print(self.blip) def meth7(self): """test one line block opening disabling""" if self.blop: # pylint: disable=no-member # error - print self.blip + print(self.blip) else: # error - print self.blip + print(self.blip) # error - print self.blip + print(self.blip) def meth8(self): """test late disabling""" # error - print self.blip + print(self.blip) # pylint: disable=no-member # no error - print self.bla - print self.blop + print(self.bla) + print(self.blop) diff --git a/src/testMultiRootWkspc/workspace2/.vscode/settings.json b/src/testMultiRootWkspc/workspace2/.vscode/settings.json index 3705457b09a7..0967ef424bce 100644 --- a/src/testMultiRootWkspc/workspace2/.vscode/settings.json +++ b/src/testMultiRootWkspc/workspace2/.vscode/settings.json @@ -1,4 +1 @@ -{ - "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 index 439f899e9e22..6aceaad5e020 100644 --- a/src/testMultiRootWkspc/workspace2/file.py +++ b/src/testMultiRootWkspc/workspace2/file.py @@ -10,78 +10,78 @@ def __init__(self): def meth1(self, arg): """this issues a message""" - print self + print(self) def meth2(self, arg): """and this one not""" # pylint: disable=unused-argument - print self\ - + "foo" + print(self\ + + "foo") def meth3(self): """test one line disabling""" # no error - print self.bla # pylint: disable=no-member + print(self.bla) # pylint: disable=no-member # error - print self.blop + print(self.blop) def meth4(self): """test re-enabling""" # pylint: disable=no-member # no error - print self.bla - print self.blop + print(self.bla) + print(self.blop) # pylint: enable=no-member # error - print self.blip + print(self.blip) def meth5(self): """test IF sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print(self.bla) if self.blop: # pylint: enable=no-member # error - print self.blip + print(self.blip) else: # no error - print self.blip + print(self.blip) # no error - print self.blip + print(self.blip) def meth6(self): """test TRY/EXCEPT sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print(self.bla) try: # pylint: enable=no-member # error - print self.blip + print(self.blip) except UndefinedName: # pylint: disable=undefined-variable # no error - print self.blip + print(self.blip) # no error - print self.blip + print(self.blip) def meth7(self): """test one line block opening disabling""" if self.blop: # pylint: disable=no-member # error - print self.blip + print(self.blip) else: # error - print self.blip + print(self.blip) # error - print self.blip + print(self.blip) def meth8(self): """test late disabling""" # error - print self.blip + print(self.blip) # pylint: disable=no-member # no error - print self.bla - print self.blop + print(self.bla) + print(self.blop) diff --git a/src/testMultiRootWkspc/workspace2/workspace2.tags.file b/src/testMultiRootWkspc/workspace2/workspace2.tags.file deleted file mode 100644 index 375785e2a94e..000000000000 --- a/src/testMultiRootWkspc/workspace2/workspace2.tags.file +++ /dev/null @@ -1,24 +0,0 @@ -!_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 index 61aa87c55fed..9e56bf5cc589 100644 --- a/src/testMultiRootWkspc/workspace2/workspace2File.py +++ b/src/testMultiRootWkspc/workspace2/workspace2File.py @@ -2,6 +2,7 @@ __revision__ = None + class Workspace2Class(object): """block-disable test""" @@ -10,4 +11,4 @@ def __init__(self): def meth1OfWorkspace2(self, arg): """this issues a message""" - print (self) + print(self) diff --git a/src/testMultiRootWkspc/workspace3/.vscode/settings.json b/src/testMultiRootWkspc/workspace3/.vscode/settings.json index 8779a0c08efe..0967ef424bce 100644 --- a/src/testMultiRootWkspc/workspace3/.vscode/settings.json +++ b/src/testMultiRootWkspc/workspace3/.vscode/settings.json @@ -1,3 +1 @@ -{ - "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" -} +{} diff --git a/src/testMultiRootWkspc/workspace3/file.py b/src/testMultiRootWkspc/workspace3/file.py index 439f899e9e22..6aceaad5e020 100644 --- a/src/testMultiRootWkspc/workspace3/file.py +++ b/src/testMultiRootWkspc/workspace3/file.py @@ -10,78 +10,78 @@ def __init__(self): def meth1(self, arg): """this issues a message""" - print self + print(self) def meth2(self, arg): """and this one not""" # pylint: disable=unused-argument - print self\ - + "foo" + print(self\ + + "foo") def meth3(self): """test one line disabling""" # no error - print self.bla # pylint: disable=no-member + print(self.bla) # pylint: disable=no-member # error - print self.blop + print(self.blop) def meth4(self): """test re-enabling""" # pylint: disable=no-member # no error - print self.bla - print self.blop + print(self.bla) + print(self.blop) # pylint: enable=no-member # error - print self.blip + print(self.blip) def meth5(self): """test IF sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print(self.bla) if self.blop: # pylint: enable=no-member # error - print self.blip + print(self.blip) else: # no error - print self.blip + print(self.blip) # no error - print self.blip + print(self.blip) def meth6(self): """test TRY/EXCEPT sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print(self.bla) try: # pylint: enable=no-member # error - print self.blip + print(self.blip) except UndefinedName: # pylint: disable=undefined-variable # no error - print self.blip + print(self.blip) # no error - print self.blip + print(self.blip) def meth7(self): """test one line block opening disabling""" if self.blop: # pylint: disable=no-member # error - print self.blip + print(self.blip) else: # error - print self.blip + print(self.blip) # error - print self.blip + print(self.blip) def meth8(self): """test late disabling""" # error - print self.blip + print(self.blip) # pylint: disable=no-member # no error - print self.bla - print self.blop + print(self.bla) + print(self.blop) diff --git a/src/testMultiRootWkspc/workspace3/workspace3.tags.file b/src/testMultiRootWkspc/workspace3/workspace3.tags.file deleted file mode 100644 index 3a65841e2aff..000000000000 --- a/src/testMultiRootWkspc/workspace3/workspace3.tags.file +++ /dev/null @@ -1,19 +0,0 @@ -!_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/workspace5/djangoApp/mysite/settings.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py index 4e182517ca2a..253f3ce20a99 100644 --- a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py @@ -19,66 +19,60 @@ # 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'] +ALLOWED_HOSTS = ["localhost", "127.0.0.1"] # Application definition INSTALLED_APPS = [ - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.contenttypes", + "django.contrib.messages", + "django.contrib.staticfiles", ] -MIDDLEWARE = [ -] +MIDDLEWARE = [] -ROOT_URLCONF = 'mysite.urls' +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', + "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' +WSGI_APPLICATION = "mysite.wsgi.application" # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases -DATABASES = { -} +DATABASES = {} # Password validation # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators -AUTH_PASSWORD_VALIDATORS = [ -] +AUTH_PASSWORD_VALIDATORS = [] # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -90,4 +84,4 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" diff --git a/src/testTestingRootWkspc/coverageWorkspace/even.py b/src/testTestingRootWkspc/coverageWorkspace/even.py new file mode 100644 index 000000000000..e395b024ecc5 --- /dev/null +++ b/src/testTestingRootWkspc/coverageWorkspace/even.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def number_type(n: int) -> str: + if n % 2 == 0: + return "even" + return "odd" diff --git a/src/testTestingRootWkspc/coverageWorkspace/test_even.py b/src/testTestingRootWkspc/coverageWorkspace/test_even.py new file mode 100644 index 000000000000..ca78535860f4 --- /dev/null +++ b/src/testTestingRootWkspc/coverageWorkspace/test_even.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from even import number_type +import unittest + + +class TestNumbers(unittest.TestCase): + def test_odd(self): + n = number_type(1) + assert n == "odd" diff --git a/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py new file mode 100644 index 000000000000..5aac911b575a --- /dev/null +++ b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + +ctypes.string_at(0) # Dereference a NULL pointer + + +class TestSegmentationFault(unittest.TestCase): + def test_segfault(self): + assert True + + +if __name__ == "__main__": + unittest.main() diff --git a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py new file mode 100644 index 000000000000..80be80f023c2 --- /dev/null +++ b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + + +class TestSegmentationFault(unittest.TestCase): + def cause_segfault(self): + print("Causing a segmentation fault") + ctypes.string_at(0) # Dereference a NULL pointer + + def test_segfault(self): + self.cause_segfault() + assert True + + +if __name__ == "__main__": + unittest.main() diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py new file mode 100644 index 000000000000..40c5de531f7c --- /dev/null +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest +import unittest + + +@pytest.mark.parametrize("num", range(0, 2000)) +def test_odd_even(num): + assert num % 2 == 0 + + +class NumbersTest(unittest.TestCase): + def test_even(self): + for i in range(0, 2000): + with self.subTest(i=i): + self.assertEqual(i % 2, 0) + + +# The repeated tests below are to test the unittest communication as it hits it maximum limit of bytes. + + +class NumberedTests1(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests2(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests3(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests4(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests5(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests6(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests7(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests8(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests9(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests10(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests11(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests12(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests13(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests14(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests15(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests16(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests17(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests18(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests19(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests20(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) diff --git a/src/testTestingRootWkspc/loggingWorkspace/test_logging.py b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py new file mode 100644 index 000000000000..a3e77f06ae78 --- /dev/null +++ b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +import logging + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") diff --git a/src/testTestingRootWkspc/smallWorkspace/test_simple.py b/src/testTestingRootWkspc/smallWorkspace/test_simple.py new file mode 100644 index 000000000000..f68a0d7d0d93 --- /dev/null +++ b/src/testTestingRootWkspc/smallWorkspace/test_simple.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest +import logging +import sys + + +def test_a(caplog): + logger = logging.getLogger(__name__) + # caplog.set_level(logging.ERROR) # Set minimum log level to capture + logger.setLevel(logging.WARN) + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + assert False + + +class SimpleClass(unittest.TestCase): + def test_simple_unit(self): + print("expected printed output, stdout") + print("expected printed output, stderr", file=sys.stderr) + assert True diff --git a/src/testTestingRootWkspc/target workspace/custom_sub_folder/test_simple.py b/src/testTestingRootWkspc/target workspace/custom_sub_folder/test_simple.py new file mode 100644 index 000000000000..179d6420c76f --- /dev/null +++ b/src/testTestingRootWkspc/target workspace/custom_sub_folder/test_simple.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +class SimpleClass(unittest.TestCase): + def test_simple_unit(self): + assert True diff --git a/tsconfig.browser.json b/tsconfig.browser.json new file mode 100644 index 000000000000..e34f3f6788ac --- /dev/null +++ b/tsconfig.browser.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./src/client/browser", + "./types", + "./typings/*.d.ts", + ] +} diff --git a/tsconfig.datascience-ui.json b/tsconfig.datascience-ui.json deleted file mode 100644 index d15dc2121b44..000000000000 --- a/tsconfig.datascience-ui.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "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 index d29570404119..d5805806b675 100644 --- a/tsconfig.extension.json +++ b/tsconfig.extension.json @@ -7,9 +7,9 @@ "lib": [ "es6", "es2018", - "ES2019" + "ES2019", + "ES2020", ], - "jsx": "react", "sourceMap": true, "rootDir": "src", "experimentalDecorators": true, @@ -20,7 +20,6 @@ "node_modules", ".vscode-test", ".vscode test", - "src/datascience-ui", "build" ] } diff --git a/tsconfig.json b/tsconfig.json index f1e8c222bfbc..718d4ab4aad1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,11 +4,17 @@ "paths": { "*": ["types/*"] }, - "module": "commonjs", + "module": "NodeNext", + "moduleResolution": "NodeNext", "target": "es2018", "outDir": "out", - "lib": ["es6", "es2018", "dom", "ES2019"], - "jsx": "react", + "lib": [ + "es6", + "es2018", + "dom", + "ES2019", + "ES2020" + ], "sourceMap": true, "rootDir": "src", "experimentalDecorators": true, @@ -18,7 +24,9 @@ "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "removeComments": true }, "exclude": [ "node_modules", @@ -28,12 +36,10 @@ "src/client/node_modules", "src/server/src/typings", "src/client/src/typings", - "src/ipywidgets", "src/smoke", - "src/test/datascience/extensionapi", "build", "out", - "ipywidgets", - "tmp" + "tmp", + "pythonExtensionApi" ] } diff --git a/tslint.json b/tslint.json deleted file mode 100644 index 0b83af565aa2..000000000000 --- a/tslint.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "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 deleted file mode 100644 index ac38b46b19d4..000000000000 --- a/types/@nteract/transform-dataresource.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 6993907366e2..000000000000 --- a/types/@nteract/transform-geojson.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index c173dbbd65ab..000000000000 --- a/types/@nteract/transform-model-debug.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index d647dcd7775c..000000000000 --- a/types/@nteract/transform-plotly.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index d98c5141a4f6..000000000000 --- a/types/@nteract/transform-vsdom.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 846e5ef8267a..000000000000 --- a/types/@nteract/transforms.d.ts +++ /dev/null @@ -1,106 +0,0 @@ -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 deleted file mode 100644 index c35b0f3c8439..000000000000 --- a/types/ansi-to-html.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 9f9c9fe87481..000000000000 --- a/types/react-data-grid.d.ts +++ /dev/null @@ -1,709 +0,0 @@ -// 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 deleted file mode 100644 index 7d65bb5b3027..000000000000 --- a/types/react-svg-pan-zoom.d.ts +++ /dev/null @@ -1,215 +0,0 @@ -// 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 deleted file mode 100644 index 4cee2e8d6dfa..000000000000 --- a/types/react-svgmt.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index a0a0c35a36a0..000000000000 --- a/types/react-tabulator.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 91c10af275d6..000000000000 --- a/types/slickgrid/index.d.ts +++ /dev/null @@ -1,1812 +0,0 @@ -// 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 deleted file mode 100644 index 2c33e524311a..000000000000 --- a/types/slickgrid/plugins/slick.autotooltips.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index ab97e4612389..000000000000 --- a/types/slickgrid/plugins/slick.checkboxselectcolumn.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index e0a0269282de..000000000000 --- a/types/slickgrid/plugins/slick.columnpicker.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index a4191a79f36a..000000000000 --- a/types/slickgrid/plugins/slick.headerbuttons.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// 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 deleted file mode 100644 index 2c06f6359133..000000000000 --- a/types/slickgrid/plugins/slick.rowselectionmodel.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 46e748682709..000000000000 --- a/types/svg-inline-react.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index a391b20d77af..000000000000 --- a/types/vscode-proposed/index.d.ts +++ /dev/null @@ -1,587 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { - Event, - GlobPattern, - Uri, - TextDocument, - ViewColumn, - CancellationToken, - Disposable, - DocumentSelector, - ProviderResult -} 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 if the content of a cell is editable or not. - */ - 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; - - /** - * 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; - 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 fileName: string; - readonly viewType: string; - readonly isDirty: boolean; - readonly cells: NotebookCell[]; - languages: string[]; - displayOrder?: GlobPattern[]; - 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 NotebookEditorCellEdit { - insert( - index: number, - content: string | string[], - language: string, - type: CellKind, - outputs: CellOutput[], - metadata: NotebookCellMetadata | undefined - ): void; - delete(index: number): void; -} - -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 column in which this editor shows. - */ - 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>; -} - -export interface NotebookOutputSelector { - mimeTypes?: string[]; -} - -export interface NotebookRenderRequest { - output: CellDisplayOutput; - mimeType: string; - outputId: string; -} - -export interface NotebookOutputRenderer { - /** - * - * @returns HTML fragment. We can probably return `CellOutput` instead of string ? - * - */ - render(document: NotebookDocument, request: NotebookRenderRequest): string; - - /** - * Call before HTML from the renderer is executed, and will be called for - * every editor associated with notebook documents where the renderer - * is or was used. - * - * The communication object will only send and receive messages to the - * render API, retrieved via `acquireNotebookRendererApi`, acquired with - * this specific renderer's ID. - * - * If you need to keep an association between the communication object - * and the document for use in the `render()` method, you can use a WeakMap. - */ - resolveNotebook?(document: NotebookDocument, communication: NotebookCommunication): void; - - readonly preloads?: Uri[]; -} - -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 NotebookCellData { - readonly cellKind: CellKind; - readonly source: string; - language: string; - outputs: CellOutput[]; - metadata: NotebookCellMetadata; -} - -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>; - - kernel?: NotebookKernel; -} - -export interface NotebookKernel { - readonly id?: string; - label: string; - description?: 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; - filenamePattern?: GlobPattern; - excludeFileNamePattern?: GlobPattern; -} - -export interface NotebookKernelProvider<T extends NotebookKernel = NotebookKernel> { - onDidChangeKernels?: Event<void>; - provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult<T[]>; - resolveKernel?( - kernel: T, - document: NotebookDocument, - webview: NotebookCommunication, - token: CancellationToken - ): ProviderResult<void>; -} - -export namespace notebook { - export function registerNotebookContentProvider( - notebookType: string, - provider: NotebookContentProvider - ): Disposable; - - export function registerNotebookKernelProvider( - selector: NotebookDocumentFilter, - provider: NotebookKernelProvider - ): Disposable; - - export function registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable; - - export function registerNotebookOutputRenderer( - id: string, - outputSelector: NotebookOutputSelector, - renderer: NotebookOutputRenderer - ): Disposable; - - export const onDidOpenNotebookDocument: Event<NotebookDocument>; - export const onDidCloseNotebookDocument: Event<NotebookDocument>; - - /** - * All currently known notebook documents. - */ - export const notebookDocuments: ReadonlyArray<NotebookDocument>; - - export let visibleNotebookEditors: NotebookEditor[]; - export const onDidChangeVisibleNotebookEditors: Event<NotebookEditor[]>; - - export let activeNotebookEditor: NotebookEditor | undefined; - export const onDidChangeActiveNotebookEditor: Event<NotebookEditor | undefined>; - export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>; - export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>; - export const onDidChangeCellLanguage: Event<NotebookCellLanguageChangeEvent>; - /** - * 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; - }>; -} diff --git a/types/vscode.proposed.d.ts b/types/vscode.proposed.d.ts deleted file mode 100644 index 7a6168ccd72d..000000000000 --- a/types/vscode.proposed.d.ts +++ /dev/null @@ -1,581 +0,0 @@ -// 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 if the content of a cell is editable or not. - */ - 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; - - /** - * 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; - 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 fileName: string; - readonly viewType: string; - readonly isDirty: boolean; - readonly cells: NotebookCell[]; - languages: string[]; - displayOrder?: GlobPattern[]; - 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 NotebookEditorCellEdit { - insert( - index: number, - content: string | string[], - language: string, - type: CellKind, - outputs: CellOutput[], - metadata: NotebookCellMetadata | undefined - ): void; - delete(index: number): void; - } - - 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 column in which this editor shows. - */ - 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>; - } - - export interface NotebookOutputSelector { - mimeTypes?: string[]; - } - - export interface NotebookRenderRequest { - output: CellDisplayOutput; - mimeType: string; - outputId: string; - } - - export interface NotebookOutputRenderer { - /** - * - * @returns HTML fragment. We can probably return `CellOutput` instead of string ? - * - */ - render(document: NotebookDocument, request: NotebookRenderRequest): string; - - /** - * Call before HTML from the renderer is executed, and will be called for - * every editor associated with notebook documents where the renderer - * is or was used. - * - * The communication object will only send and receive messages to the - * render API, retrieved via `acquireNotebookRendererApi`, acquired with - * this specific renderer's ID. - * - * If you need to keep an association between the communication object - * and the document for use in the `render()` method, you can use a WeakMap. - */ - resolveNotebook?(document: NotebookDocument, communication: NotebookCommunication): void; - - readonly preloads?: Uri[]; - } - - 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 NotebookCellData { - readonly cellKind: CellKind; - readonly source: string; - language: string; - outputs: CellOutput[]; - metadata: NotebookCellMetadata; - } - - 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>; - - kernel?: NotebookKernel; - } - - export interface NotebookKernel { - readonly id?: string; - label: string; - description?: 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; - filenamePattern?: GlobPattern; - excludeFileNamePattern?: GlobPattern; - } - - export interface NotebookKernelProvider<T extends NotebookKernel = NotebookKernel> { - onDidChangeKernels?: Event<void>; - provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult<T[]>; - resolveKernel?( - kernel: T, - document: NotebookDocument, - webview: NotebookCommunication, - token: CancellationToken - ): ProviderResult<void>; - } - - export namespace notebook { - export function registerNotebookContentProvider( - notebookType: string, - provider: NotebookContentProvider - ): Disposable; - - export function registerNotebookKernelProvider( - selector: NotebookDocumentFilter, - provider: NotebookKernelProvider - ): Disposable; - - export function registerNotebookKernel( - id: string, - selectors: GlobPattern[], - kernel: NotebookKernel - ): Disposable; - - export function registerNotebookOutputRenderer( - id: string, - outputSelector: NotebookOutputSelector, - renderer: NotebookOutputRenderer - ): Disposable; - - export const onDidOpenNotebookDocument: Event<NotebookDocument>; - export const onDidCloseNotebookDocument: Event<NotebookDocument>; - - /** - * All currently known notebook documents. - */ - export const notebookDocuments: ReadonlyArray<NotebookDocument>; - - export let visibleNotebookEditors: NotebookEditor[]; - export const onDidChangeVisibleNotebookEditors: Event<NotebookEditor[]>; - - export let activeNotebookEditor: NotebookEditor | undefined; - export const onDidChangeActiveNotebookEditor: Event<NotebookEditor | undefined>; - export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>; - export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>; - export const onDidChangeCellLanguage: Event<NotebookCellLanguageChangeEvent>; - /** - * 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; - }>; - } -} diff --git a/types/vscode.proposed.envCollectionOptions.d.ts b/types/vscode.proposed.envCollectionOptions.d.ts new file mode 100644 index 000000000000..d25a92725a4d --- /dev/null +++ b/types/vscode.proposed.envCollectionOptions.d.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/179476 + + /** + * Options applied to the mutator. + */ + export interface EnvironmentVariableMutatorOptions { + /** + * Apply to the environment just before the process is created. + * + * Defaults to true. + */ + applyAtProcessCreation?: boolean; + + /** + * Apply to the environment in the shell integration script. Note that this _will not_ apply + * the mutator if shell integration is disabled or not working for some reason. + * + * Defaults to false. + */ + applyAtShellIntegration?: boolean; + } + + /** + * A type of mutation and its value to be applied to an environment variable. + */ + export interface EnvironmentVariableMutator { + /** + * Options applied to the mutator. + */ + readonly options: EnvironmentVariableMutatorOptions; + } + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * @param options Options applied to the mutator. + */ + replace(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + append(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + prepend(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + } +} diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts new file mode 100644 index 000000000000..a03a639b5ee2 --- /dev/null +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/171173 + + // export interface ExtensionContext { + // /** + // * Gets the extension's global environment variable collection for this workspace, enabling changes to be + // * applied to terminal environment variables. + // */ + // readonly environmentVariableCollection: GlobalEnvironmentVariableCollection; + // } + + export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection { + /** + * Gets scope-specific environment variable collection for the extension. This enables alterations to + * terminal environment variables solely within the designated scope, and is applied in addition to (and + * after) the global collection. + * + * Each object obtained through this method is isolated and does not impact objects for other scopes, + * including the global collection. + * + * @param scope The scope to which the environment variable collection applies to. + */ + getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + } +} diff --git a/types/vscode.proposed.notebookReplDocument.d.ts b/types/vscode.proposed.notebookReplDocument.d.ts new file mode 100644 index 000000000000..d78450e944a8 --- /dev/null +++ b/types/vscode.proposed.notebookReplDocument.d.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface NotebookDocumentShowOptions { + /** + * The notebook should be opened in a REPL editor, + * where the last cell of the notebook is an input box and the other cells are the read-only history. + * When the value is a string, it will be used as the label for the editor tab. + */ + readonly asRepl?: boolean | string | { + /** + * The label to be used for the editor tab. + */ + readonly label: string; + }; + } + + export interface NotebookEditor { + /** + * Information about the REPL editor if the notebook was opened as a repl. + */ + replOptions?: { + /** + * The index where new cells should be appended. + */ + appendIndex: number; + }; + } +} diff --git a/types/vscode.proposed.notebookVariableProvider.d.ts b/types/vscode.proposed.notebookVariableProvider.d.ts new file mode 100644 index 000000000000..4fac96c45f0a --- /dev/null +++ b/types/vscode.proposed.notebookVariableProvider.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare module 'vscode' { + + export interface NotebookController { + /** Set this to attach a variable provider to this controller. */ + variableProvider?: NotebookVariableProvider; + } + + export enum NotebookVariablesRequestKind { + Named = 1, + Indexed = 2 + } + + interface VariablesResult { + variable: Variable; + hasNamedChildren: boolean; + indexedChildrenCount: number; + } + + interface NotebookVariableProvider { + onDidChangeVariables: Event<NotebookDocument>; + + /** When parent is undefined, this is requesting global Variables. When a variable is passed, it's requesting child props of that Variable. */ + provideVariables(notebook: NotebookDocument, parent: Variable | undefined, kind: NotebookVariablesRequestKind, start: number, token: CancellationToken): AsyncIterable<VariablesResult>; + } + + interface Variable { + /** The variable's name. */ + name: string; + + /** The variable's value. + This can be a multi-line text, e.g. for a function the body of a function. + For structured variables (which do not have a simple value), it is recommended to provide a one-line representation of the structured object. + This helps to identify the structured object in the collapsed state when its children are not yet visible. + An empty string can be used if no value should be shown in the UI. + */ + value: string; + + /** The code that represents how the variable would be accessed in the runtime environment */ + expression?: string; + + /** The type of the variable's value */ + type?: string; + + /** The interfaces or contracts that the type satisfies */ + interfaces?: string[]; + + /** The language of the variable's value */ + language?: string; + } + +} diff --git a/types/vscode.proposed.quickPickSortByLabel.d.ts b/types/vscode.proposed.quickPickSortByLabel.d.ts new file mode 100644 index 000000000000..405d67671d78 --- /dev/null +++ b/types/vscode.proposed.quickPickSortByLabel.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/73904 + + export interface QuickPick<T extends QuickPickItem> extends QuickInput { + /** + * An optional flag to sort the final results by index of first query match in label. Defaults to true. + */ + sortByLabel: boolean; + } +} diff --git a/types/vscode.proposed.testObserver.d.ts b/types/vscode.proposed.testObserver.d.ts new file mode 100644 index 000000000000..2bdb21d74732 --- /dev/null +++ b/types/vscode.proposed.testObserver.d.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/107467 + + export namespace tests { + /** + * Requests that tests be run by their controller. + * @param run Run options to use. + * @param token Cancellation token for the test run + */ + export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable<void>; + + /** + * Returns an observer that watches and can request tests. + */ + export function createTestObserver(): TestObserver; + /** + * List of test results stored by the editor, sorted in descending + * order by their `completedAt` time. + */ + export const testResults: ReadonlyArray<TestRunResult>; + + /** + * Event that fires when the {@link testResults} array is updated. + */ + export const onDidChangeTestResults: Event<void>; + } + + export interface TestObserver { + /** + * List of tests returned by test provider for files in the workspace. + */ + readonly tests: ReadonlyArray<TestItem>; + + /** + * An event that fires when an existing test in the collection changes, or + * null if a top-level test was added or removed. When fired, the consumer + * should check the test item and all its children for changes. + */ + readonly onDidChangeTest: Event<TestsChangeEvent>; + + /** + * Dispose of the observer, allowing the editor to eventually tell test + * providers that they no longer need to update tests. + */ + dispose(): void; + } + + export interface TestsChangeEvent { + /** + * List of all tests that are newly added. + */ + readonly added: ReadonlyArray<TestItem>; + + /** + * List of existing tests that have updated. + */ + readonly updated: ReadonlyArray<TestItem>; + + /** + * List of existing tests that have been removed. + */ + readonly removed: ReadonlyArray<TestItem>; + } + + /** + * A test item is an item shown in the "test explorer" view. It encompasses + * both a suite and a test, since they have almost or identical capabilities. + */ + export interface TestItem { + /** + * Marks the test as outdated. This can happen as a result of file changes, + * for example. In "auto run" mode, tests that are outdated will be + * automatically rerun after a short delay. Invoking this on a + * test with children will mark the entire subtree as outdated. + * + * Extensions should generally not override this method. + */ + // todo@api still unsure about this + invalidateResults(): void; + } + + + /** + * TestResults can be provided to the editor in {@link tests.publishTestResult}, + * or read from it in {@link tests.testResults}. + * + * The results contain a 'snapshot' of the tests at the point when the test + * run is complete. Therefore, information such as its {@link Range} may be + * out of date. If the test still exists in the workspace, consumers can use + * its `id` to correlate the result instance with the living test. + */ + export interface TestRunResult { + /** + * Unix milliseconds timestamp at which the test run was completed. + */ + readonly completedAt: number; + + /** + * Optional raw output from the test run. + */ + readonly output?: string; + + /** + * List of test results. The items in this array are the items that + * were passed in the {@link tests.runTests} method. + */ + readonly results: ReadonlyArray<Readonly<TestResultSnapshot>>; + } + + /** + * A {@link TestItem}-like interface with an associated result, which appear + * or can be provided in {@link TestResult} interfaces. + */ + export interface TestResultSnapshot { + /** + * Unique identifier that matches that of the associated TestItem. + * This is used to correlate test results and tests in the document with + * those in the workspace (test explorer). + */ + readonly id: string; + + /** + * Parent of this item. + */ + readonly parent?: TestResultSnapshot; + + /** + * URI this TestItem is associated with. May be a file or file. + */ + readonly uri?: Uri; + + /** + * Display name describing the test case. + */ + readonly label: string; + + /** + * Optional description that appears next to the label. + */ + readonly description?: string; + + /** + * Location of the test item in its `uri`. This is only meaningful if the + * `uri` points to a file. + */ + readonly range?: Range; + + /** + * State of the test in each task. In the common case, a test will only + * be executed in a single task and the length of this array will be 1. + */ + readonly taskStates: ReadonlyArray<TestSnapshotTaskState>; + + /** + * Optional list of nested tests for this item. + */ + readonly children: Readonly<TestResultSnapshot>[]; + } + + export interface TestSnapshotTaskState { + /** + * Current result of the test. + */ + readonly state: TestResultState; + + /** + * The number of milliseconds the test took to run. This is set once the + * `state` is `Passed`, `Failed`, or `Errored`. + */ + readonly duration?: number; + + /** + * Associated test run message. Can, for example, contain assertion + * failure information if the test fails. + */ + readonly messages: ReadonlyArray<TestMessage>; + } + + /** + * Possible states of tests in a test run. + */ + export enum TestResultState { + // Test will be run, but is not currently running. + Queued = 1, + // Test is currently running + Running = 2, + // Test run has passed + Passed = 3, + // Test run has failed (on an assertion) + Failed = 4, + // Test run has been skipped + Skipped = 5, + // Test run failed for some other reason (compilation error, timeout, etc) + Errored = 6 + } +} diff --git a/typings/dom.fix.rx.compiler.d.ts b/typings/dom.fix.rx.compiler.d.ts index 64ced3161585..b6779426a7ac 100644 --- a/typings/dom.fix.rx.compiler.d.ts +++ b/typings/dom.fix.rx.compiler.d.ts @@ -6,7 +6,7 @@ * 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 { } diff --git a/typings/extensions.d.ts b/typings/extensions.d.ts index 4a423f329d57..f12b718c4b10 100644 --- a/typings/extensions.d.ts +++ b/typings/extensions.d.ts @@ -2,30 +2,30 @@ // 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. -*/ + * @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[]; + 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; + toCommandArgumentForPythonExt(): 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; + fileToCommandArgumentForPythonExt(): string; /** * String.format() implementation. * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. @@ -38,10 +38,9 @@ declare interface String { trimQuotes(): string; } -// tslint:disable-next-line:interface-name declare interface Promise<T> { /** * Catches task errors and ignores them. */ - ignoreErrors(): void; + ignoreErrors(): Promise<void>; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 26538bbff554..7003ea5043b7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,163 +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; -} - -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); - } +declare namespace JQuery { + type TriggeredEvent = unknown; } diff --git a/typings/vscode-proposed/index.d.ts b/typings/vscode-proposed/index.d.ts index a391b20d77af..27d76adca192 100644 --- a/typings/vscode-proposed/index.d.ts +++ b/typings/vscode-proposed/index.d.ts @@ -1,587 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { - Event, - GlobPattern, - Uri, - TextDocument, - ViewColumn, - CancellationToken, - Disposable, - DocumentSelector, - ProviderResult -} from 'vscode'; +/* eslint-disable */ -// 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 if the content of a cell is editable or not. - */ - 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; - - /** - * 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; - 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 fileName: string; - readonly viewType: string; - readonly isDirty: boolean; - readonly cells: NotebookCell[]; - languages: string[]; - displayOrder?: GlobPattern[]; - 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 NotebookEditorCellEdit { - insert( - index: number, - content: string | string[], - language: string, - type: CellKind, - outputs: CellOutput[], - metadata: NotebookCellMetadata | undefined - ): void; - delete(index: number): void; -} - -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 column in which this editor shows. - */ - 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>; -} - -export interface NotebookOutputSelector { - mimeTypes?: string[]; -} - -export interface NotebookRenderRequest { - output: CellDisplayOutput; - mimeType: string; - outputId: string; -} - -export interface NotebookOutputRenderer { - /** - * - * @returns HTML fragment. We can probably return `CellOutput` instead of string ? - * - */ - render(document: NotebookDocument, request: NotebookRenderRequest): string; - - /** - * Call before HTML from the renderer is executed, and will be called for - * every editor associated with notebook documents where the renderer - * is or was used. - * - * The communication object will only send and receive messages to the - * render API, retrieved via `acquireNotebookRendererApi`, acquired with - * this specific renderer's ID. - * - * If you need to keep an association between the communication object - * and the document for use in the `render()` method, you can use a WeakMap. - */ - resolveNotebook?(document: NotebookDocument, communication: NotebookCommunication): void; - - readonly preloads?: Uri[]; -} - -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 NotebookCellData { - readonly cellKind: CellKind; - readonly source: string; - language: string; - outputs: CellOutput[]; - metadata: NotebookCellMetadata; -} - -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>; - - kernel?: NotebookKernel; -} - -export interface NotebookKernel { - readonly id?: string; - label: string; - description?: 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; - filenamePattern?: GlobPattern; - excludeFileNamePattern?: GlobPattern; -} - -export interface NotebookKernelProvider<T extends NotebookKernel = NotebookKernel> { - onDidChangeKernels?: Event<void>; - provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult<T[]>; - resolveKernel?( - kernel: T, - document: NotebookDocument, - webview: NotebookCommunication, - token: CancellationToken - ): ProviderResult<void>; -} - -export namespace notebook { - export function registerNotebookContentProvider( - notebookType: string, - provider: NotebookContentProvider - ): Disposable; - - export function registerNotebookKernelProvider( - selector: NotebookDocumentFilter, - provider: NotebookKernelProvider - ): Disposable; - - export function registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable; - - export function registerNotebookOutputRenderer( - id: string, - outputSelector: NotebookOutputSelector, - renderer: NotebookOutputRenderer - ): Disposable; - - export const onDidOpenNotebookDocument: Event<NotebookDocument>; - export const onDidCloseNotebookDocument: Event<NotebookDocument>; - - /** - * All currently known notebook documents. - */ - export const notebookDocuments: ReadonlyArray<NotebookDocument>; - - export let visibleNotebookEditors: NotebookEditor[]; - export const onDidChangeVisibleNotebookEditors: Event<NotebookEditor[]>; - - export let activeNotebookEditor: NotebookEditor | undefined; - export const onDidChangeActiveNotebookEditor: Event<NotebookEditor | undefined>; - export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>; - export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>; - export const onDidChangeCellLanguage: Event<NotebookCellLanguageChangeEvent>; - /** - * 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; - }>; -} +/* Proposed APIS can go here */ diff --git a/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts b/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts new file mode 100644 index 000000000000..4e7d00fa5edf --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/73904 + + export interface QuickPickItem { + /** + * An optional flag to sort the final results by index of first query match in label. Defaults to true. + */ + tooltip?: string | MarkdownString; + } +} diff --git a/typings/vscode-proposed/vscode.proposed.saveEditor.d.ts b/typings/vscode-proposed/vscode.proposed.saveEditor.d.ts new file mode 100644 index 000000000000..9088939a4649 --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.saveEditor.d.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// https://github.com/microsoft/vscode/issues/178713 + +declare module 'vscode' { + + export namespace workspace { + + /** + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful. + * + * **Note** that an editor with the provided resource must be opened in order to be saved. + * + * @param uri the associated uri for the opened editor to save. + * @return A thenable that resolves when the save operation has finished. + */ + export function save(uri: Uri): Thenable<Uri | undefined>; + + /** + * Saves the editor identified by the given resource to a new file name as provided by the user and + * returns the resulting resource or `undefined` if save was not successful or cancelled. + * + * **Note** that an editor with the provided resource must be opened in order to be saved as. + * + * @param uri the associated uri for the opened editor to save as. + * @return A thenable that resolves when the save-as operation has finished. + */ + export function saveAs(uri: Uri): Thenable<Uri | undefined>; + } +} diff --git a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts new file mode 100644 index 000000000000..6913b862c70f --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/78502 + // + // This API is still proposed but we don't intent on promoting it to stable due to problems + // around performance. See #145234 for a more likely API to get stabilized. + + export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; + } + + namespace window { + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + export const onDidWriteTerminalData: Event<TerminalDataWriteEvent>; + } +} diff --git a/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts new file mode 100644 index 000000000000..7f503f1aa6da --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/145234 + + export interface TerminalExecutedCommand { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + /** + * The full command line that was executed, including both the command and the arguments. + */ + commandLine: string | undefined; + /** + * The current working directory that was reported by the shell. This will be a {@link Uri} + * if the string reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + /** + * The exit code reported by the shell. + */ + exitCode: number | undefined; + /** + * The output of the command when it has finished executing. This is the plain text shown in + * the terminal buffer and does not include raw escape sequences. Depending on the shell + * setup, this may include the command line as part of the output. + */ + output: string | undefined; + } + + export namespace window { + /** + * An event that is emitted when a terminal with shell integration activated has completed + * executing a command. + * + * Note that this event will not fire if the executed command exits the shell, listen to + * {@link onDidCloseTerminal} to handle that case. + */ + export const onDidExecuteTerminalCommand: Event<TerminalExecutedCommand>; + } +} diff --git a/typings/webworker.fix.d.ts b/typings/webworker.fix.d.ts new file mode 100644 index 000000000000..80b53fb5b2e3 --- /dev/null +++ b/typings/webworker.fix.d.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Fake interfaces that are required for web workers to work around +// tsconfig's DOM and WebWorker lib options being mutally exclusive. +// https://github.com/microsoft/TypeScript/issues/20595 + +interface DedicatedWorkerGlobalScope {}